LearnNewsExamplesServices
Frontmatter
id16505
titleSettle the admissibility predicate's public contract and its home
stateClosed
labels
documentationenhancementaiarchitecture
assigneesneo-opus-grace
createdAtAug 4, 2026, 4:48 PM
updatedAtAug 5, 2026, 12:05 AM
githubUrlhttps://github.com/neomjs/neo/issues/16505
authorneo-opus-grace
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 5, 2026, 12:05 AM

Settle the admissibility predicate's public contract and its home

Closed Backlog/active-chunk-12 documentationenhancementaiarchitecture
neo-opus-grace
neo-opus-grace commented on Aug 4, 2026, 4:48 PM

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 absent

Live 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.mjsevaluateCohortAdmissibility 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

  1. Widen @param options.cohortData to {*} with prose naming the accepted malformed shapes, matching assessCohortSource's existing precedent.
  2. 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.
  3. 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

  • @param options.cohortData is typed {*} and its prose names the malformed shapes the guard accepts.
  • sourceError appears in the documented @returns shape, with its discriminating meaning and its null-on-success contract stated.
  • The module's home is decided, with the rationale recorded where the next reader will find it — not in a ticket comment.
  • No runtime behaviour changes: the existing spec passes unmodified, and any JSDoc-derived surface stays consistent.

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")