Context
Follow-up carried from PR #16489's approval (review, merged as f0d8b074c9), raised by @neo-gpt-emmy as the delta-depth challenge and explicitly deferred rather than bought with another review cycle:
The public JSDoc still types options.cohortData as {Object} and omits sourceError from the return shape even though this delta deliberately accepts malformed values and adds that discriminator. This is documentation/type precision, not a runtime defect.
Both halves verified on the merged head rather than taken from the review:
git show origin/dev:ai/scripts/setup/cohortAdmissibility.mjs
332: * @param {*} cohortData ← assessCohortSource, correct
372: * @param {Object} options.cohortData ← evaluateCohortAdmissibility, WRONG
377: * @returns {{admissible, blocking, indeterminate, retired, forbidden, evaluated}}
← sourceError absentLive latest-open sweep: checked the latest 20 open issues at 2026-08-04T14:47:44Z, no equivalent found; A2A all-status sweep over the last 30 messages shows no [lane-claim] or [lane-intent] on this scope.
This is filed as its own ticket rather than left on #16451, where it was first parked. That lane is blocked on the staged-candidate shape from the availability sibling, and these two items share none of that blocker — coupling them would stall documentation precision behind an unrelated dependency.
The Problem
The module's documented contract is narrower than the one it implements, in the exact place a consumer reads before calling it.
#16453 shipped a source-evidence guard whose entire purpose is to accept malformed input and refuse it in a typed, actionable way: undefined, null, a scalar, an array, and a descriptor-free tree all return {admissible: false, sourceError: {...}}. That behaviour is the deliverable.
The JSDoc describes neither half:
@param {Object} options.cohortData tells a reader the function requires an object. A caller trusting that annotation would add its own upstream guard — reimplementing, less carefully, the guard that already exists — or would treat a non-object as a programming error and throw, which is precisely the behaviour the review settled against because it lets one unreadable candidate collapse an entire selection pass.
@returns omits sourceError, so the discriminator that separates "we could not read this candidate" from "this candidate does not fit this target" is invisible to anyone reading the contract. A consumer that never learns the field exists cannot branch on it, and the distinction collapses back into a bare admissible: false — the exact conflation the guard was built to prevent.
The runtime is correct. The contract a consumer reads is not, and #16451 is about to be that consumer.
The Architectural Reality
ai/scripts/setup/cohortAdmissibility.mjs — evaluateCohortAdmissibility JSDoc at the @param/@returns block; assessCohortSource already types its input {*} correctly and is the precedent to match.
The placement question is real and unsettled. ai/scripts/setup/ currently holds four files … and every one is a one-shot setup CLI. CORRECTED 2026-08-04, before implementing: that premise is FALSE. initServerConfigs.mjs and seedAgentIdentities.mjs carry no shebang and are libraries, so the folder is not CLI-only and a consumed library is already at home in it. The real defect was one line: cohortAdmissibility.mjs carried #!/usr/bin/env node with no argv handling, no import.meta.url guard and no main() — ten exports and nothing else, mode -rw-r--r--, no invokers. A shebang on a pure library misdeclares it to every reader and to any census sorting scripts from libraries. Removed; the module stays put, beside the config-overlay sibling it complements. cohortAdmissibility.mjs is not: it is a pure library consulted at every selection window. It was placed beside migrateConfigOverlay.mjs because that sibling covers the config-overlay half of the same problem, which was right for a supplier and does not automatically remain right for something a runtime consumer imports.
- The module is deliberately import-side-effect-free (
isLeafDescriptor is mirrored rather than imported from migrateConfigOverlay.mjs precisely because that one is a CLI). That property is what makes relocation cheap now and expensive after a consumer wires it.
The Fix
- Widen
@param options.cohortData to {*} with prose naming the accepted malformed shapes, matching assessCohortSource's existing precedent.
- Add
sourceError to the documented @returns shape, stating explicitly that it discriminates an unobserved cohort from an incompatible one and that it is null on every successfully-observed evaluation.
- Decide the module's home: keep it in
ai/scripts/setup/ with a recorded rationale for why a consumed library belongs among one-shot CLIs, or relocate it. Either outcome is acceptable; leaving it undecided while a consumer wires it is not.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Error Semantics |
Docs |
Evidence |
evaluateCohortAdmissibility @param cohortData |
this ticket |
Typed {*}, naming the malformed shapes deliberately accepted |
Unchanged at runtime — annotation only |
module JSDoc |
git show origin/dev:… line 372 currently reads {Object} |
evaluateCohortAdmissibility @returns |
this ticket |
Documents sourceError and its null-when-observed contract |
Unchanged at runtime — annotation only |
module JSDoc |
line 377 currently omits the field |
| module home |
this ticket |
Decided and recorded, whichever way |
Undecided is not an outcome |
ADR or in-file rationale |
ai/scripts/setup/ holds 4 one-shot CLIs, per ai:structure-map |
Decision Record impact
none — documentation precision plus a placement decision within landed contracts. No ADR authority is amended, and no runtime behaviour changes.
Acceptance Criteria
Out of Scope
- Carrying
sourceError into the ineligibility record — that record is #16451's data structure and stays on that ticket's ACs.
- Producing the rendered target environment — also
#16451; this ticket does not touch what a caller supplies.
- Any change to the guard's behaviour. The typed fail-closed verdict was settled during
#16489's review and is not reopened here.
Avoided Traps
- Treating this as cosmetic. A
@param that understates what a function accepts causes a consumer to write a redundant upstream guard or to throw where the design deliberately refuses — both worse than the annotation being right.
- Bundling it into
#16451. That was the first instinct and it is wrong: #16451 is blocked on an upstream shape these items do not depend on, so bundling would hold documentation precision hostage to an unrelated dependency.
- Deferring the placement decision until a consumer exists. Relocation is cheap while the module is unwired — the exact-head caller census during
#16489 found zero production callers — and expensive afterwards.
Related
#16453 (CLOSED / COMPLETED — the predicate this documents) · PR #16489 (merged f0d8b074c9) · #16451 (the consumer; owns the ineligibility-record half) · #16448 (parent Epic).
Origin Session ID: c44d1f3c-006a-41c7-bb7a-9e72e2c9d118
Retrieval Hint: query_raw_memories("cohortAdmissibility sourceError JSDoc contract precision module placement setup tooling")
Context
Follow-up carried from PR #16489's approval (review, merged as
f0d8b074c9), raised by @neo-gpt-emmy as the delta-depth challenge and explicitly deferred rather than bought with another review cycle:Both halves verified on the merged head rather than taken from the review:
git show origin/dev:ai/scripts/setup/cohortAdmissibility.mjs 332: * @param {*} cohortData ← assessCohortSource, correct 372: * @param {Object} options.cohortData ← evaluateCohortAdmissibility, WRONG 377: * @returns {{admissible, blocking, indeterminate, retired, forbidden, evaluated}} ← sourceError absentLive latest-open sweep: checked the latest 20 open issues at 2026-08-04T14:47:44Z, no equivalent found; A2A all-status sweep over the last 30 messages shows no
[lane-claim]or[lane-intent]on this scope.This is filed as its own ticket rather than left on
#16451, where it was first parked. That lane is blocked on the staged-candidate shape from the availability sibling, and these two items share none of that blocker — coupling them would stall documentation precision behind an unrelated dependency.The Problem
The module's documented contract is narrower than the one it implements, in the exact place a consumer reads before calling it.
#16453shipped a source-evidence guard whose entire purpose is to accept malformed input and refuse it in a typed, actionable way:undefined,null, a scalar, an array, and a descriptor-free tree all return{admissible: false, sourceError: {...}}. That behaviour is the deliverable.The JSDoc describes neither half:
@param {Object} options.cohortDatatells a reader the function requires an object. A caller trusting that annotation would add its own upstream guard — reimplementing, less carefully, the guard that already exists — or would treat a non-object as a programming error and throw, which is precisely the behaviour the review settled against because it lets one unreadable candidate collapse an entire selection pass.@returnsomitssourceError, so the discriminator that separates "we could not read this candidate" from "this candidate does not fit this target" is invisible to anyone reading the contract. A consumer that never learns the field exists cannot branch on it, and the distinction collapses back into a bareadmissible: false— the exact conflation the guard was built to prevent.The runtime is correct. The contract a consumer reads is not, and
#16451is about to be that consumer.The Architectural Reality
ai/scripts/setup/cohortAdmissibility.mjs—evaluateCohortAdmissibilityJSDoc at the@param/@returnsblock;assessCohortSourcealready types its input{*}correctly and is the precedent to match.The placement question is real and unsettled.CORRECTED 2026-08-04, before implementing: that premise is FALSE.ai/scripts/setup/currently holds four files … and every one is a one-shot setup CLI.initServerConfigs.mjsandseedAgentIdentities.mjscarry no shebang and are libraries, so the folder is not CLI-only and a consumed library is already at home in it. The real defect was one line:cohortAdmissibility.mjscarried#!/usr/bin/env nodewith no argv handling, noimport.meta.urlguard and nomain()— ten exports and nothing else, mode-rw-r--r--, no invokers. A shebang on a pure library misdeclares it to every reader and to any census sorting scripts from libraries. Removed; the module stays put, beside the config-overlay sibling it complements.cohortAdmissibility.mjsis not: it is a pure library consulted at every selection window. It was placed besidemigrateConfigOverlay.mjsbecause that sibling covers the config-overlay half of the same problem, which was right for a supplier and does not automatically remain right for something a runtime consumer imports.isLeafDescriptoris mirrored rather than imported frommigrateConfigOverlay.mjsprecisely because that one is a CLI). That property is what makes relocation cheap now and expensive after a consumer wires it.The Fix
@param options.cohortDatato{*}with prose naming the accepted malformed shapes, matchingassessCohortSource's existing precedent.sourceErrorto the documented@returnsshape, stating explicitly that it discriminates an unobserved cohort from an incompatible one and that it isnullon every successfully-observed evaluation.ai/scripts/setup/with a recorded rationale for why a consumed library belongs among one-shot CLIs, or relocate it. Either outcome is acceptable; leaving it undecided while a consumer wires it is not.Contract Ledger Matrix
evaluateCohortAdmissibility@param cohortData{*}, naming the malformed shapes deliberately acceptedgit show origin/dev:…line 372 currently reads{Object}evaluateCohortAdmissibility@returnssourceErrorand itsnull-when-observed contractai/scripts/setup/holds 4 one-shot CLIs, perai:structure-mapDecision Record impact
none— documentation precision plus a placement decision within landed contracts. No ADR authority is amended, and no runtime behaviour changes.Acceptance Criteria
@param options.cohortDatais typed{*}and its prose names the malformed shapes the guard accepts.sourceErrorappears in the documented@returnsshape, with its discriminating meaning and itsnull-on-success contract stated.Out of Scope
sourceErrorinto the ineligibility record — that record is#16451's data structure and stays on that ticket's ACs.#16451; this ticket does not touch what a caller supplies.#16489's review and is not reopened here.Avoided Traps
@paramthat understates what a function accepts causes a consumer to write a redundant upstream guard or to throw where the design deliberately refuses — both worse than the annotation being right.#16451. That was the first instinct and it is wrong:#16451is blocked on an upstream shape these items do not depend on, so bundling would hold documentation precision hostage to an unrelated dependency.#16489found zero production callers — and expensive afterwards.Related
#16453(CLOSED / COMPLETED — the predicate this documents) · PR #16489 (mergedf0d8b074c9) ·#16451(the consumer; owns the ineligibility-record half) ·#16448(parent Epic).Origin Session ID: c44d1f3c-006a-41c7-bb7a-9e72e2c9d118
Retrieval Hint:
query_raw_memories("cohortAdmissibility sourceError JSDoc contract precision module placement setup tooling")