Spec Studio

Duplicate carrier IDs are excluded loudly

spec:validation.duplicate-idsUse Case / Behavior · featureready floor ready

Packs: pack:self-hosting-v1

Design: no design entries · no pinned declarations · no decisions

Findings: no errors, no warnings

specs/validation/duplicate-ids.sdp.md

No open questions and no blocking questions; implementation binding present, verifier binding present, and expected-outcome oracle none.

Intent
outcome
Prevent ambiguous authored identity from entering the graph.
Behavior
rule line 20 · specs/validation/duplicate-ids.sdp.md:20
If more than one carrier declares an ID, every duplicate site receives extract/duplicate-id and no ambiguous node is derived.
rule line 21 · specs/validation/duplicate-ids.sdp.md:21
Over a graph from any producer, every node whose id is carried by another node receives a conformance/duplicate-ids error; the extractor separately excludes ambiguous authored carriers through extract/duplicate-id.

2 unkeyed rules.

Verifying examples

TypeScript and Markdown duplicates are both refused · spec:validation.duplicate-ids.dual-carrier · ready floor ready

Executable Spec

Given a {firstCarrier: "TypeScript"} carrier declares {specId: "spec:fixture.duplicate"}
Given a {secondCarrier: "Markdown"} carrier declares {specId: "spec:fixture.duplicate"}
When the extraction root is read
Then both sites report {findingId: "extract/duplicate-id"}
Then no graph node is emitted for {specId: "spec:fixture.duplicate"}

Open example

Example space

Given a {firstCarrier:string} carrier declares {specId:string}
Given a {secondCarrier:string} carrier declares {specId:string}
When the extraction root is read
Then both sites report {findingId:string}
Then no graph node is emitted for {specId:string}
Verification

Verifier bindings

  • spec:validation.duplicate-ids.dual-carrier · TypeScript and Markdown duplicates are both refused · declared · example enabled · specs/validation/duplicate-ids.dual-carrier.sdp.md

    External run evidence from test/self-hosting-duplicate-ids.test.ts

    Test anchors: test:protocol.duplicate-ids.dual-carrier. Specs verified by those bindings: spec:validation.duplicate-ids, spec:validation.duplicate-ids.dual-carrier.

    Run 1: external run evidence from vitest 4.1.10

    Build: CI 433 · Branch: fix/second-review-pass · Commit: 6375b6e431a7cf885a49befec76d7876cd3e538c · Start: 2026-11-04T07:12:23.076Z · Stop: 2026-11-04T07:15:49.695Z

    NameSuiteResultDuration
    names self-hosting generation before collecting its filtered tracergenerated contract preflightpassed420.97 ms
    names checkout generation before collecting its filtered examplegenerated contract preflightpassed406.66 ms
    names both generations when an unfiltered suite lacks both contract treesgenerated contract preflightpassed382.54 ms
    matches a Vitest substring filter before collecting the self-hosting tracergenerated contract preflightpassed433.99 ms
    matches a ./-prefixed filter before collecting the self-hosting tracergenerated contract preflightpassed419.01 ms
    matches an absolute-path filter before collecting the self-hosting tracergenerated contract preflightpassed412.9 ms
    reads an option value ["--reporter","verbose"] as no file filter, so the whole suite's contracts are requiredgenerated contract preflightpassed489.56 ms
    reads an option value ["--maxWorkers","1"] as no file filter, so the whole suite's contracts are requiredgenerated contract preflightpassed447.65 ms
    reads an option value ["-t","duplicate"] as no file filter, so the whole suite's contracts are requiredgenerated contract preflightpassed448.61 ms
    matches a file:line filter by its file before collecting the self-hosting tracergenerated contract preflightpassed470.09 ms
    requires every contract tree for `related`, whose source paths name no test file (present: ["examples/checkout-v1/generated/contracts"])generated contract preflightpassed412.33 ms
    requires every contract tree for `related`, whose source paths name no test file (present: ["generated/contracts"])generated contract preflightpassed437.48 ms
    does not require contracts for an unrelated Vitest filtergenerated contract preflightpassed487.81 ms
    proceeds for the unfiltered invocation once both contract trees are generatedgenerated contract preflightpassed472.5 ms
    fails a pooled run that mutates repository-root generated statepooled root generated-state sentinelpassed447.55 ms
    keeps the dedicated CLI pass when an option carries a separate value, and forwards it therepooled root generated-state sentinelpassed526.97 ms
    routes ["--","test/bootstrap.test.ts"] without running test/cli.test.ts in the poolpooled root generated-state sentinelpassed454.43 ms
    routes ["--","--reporter","dot"] without running test/cli.test.ts in the poolpooled root generated-state sentinelpassed514.99 ms
    routes ["related","src/cli/validate-watch.ts"] without running test/cli.test.ts in the poolpooled root generated-state sentinelpassed575.58 ms
    routes ["test/cli.test.ts:120"] without running test/cli.test.ts in the poolpooled root generated-state sentinelpassed424.18 ms
    routes ["test/bootstrap.test.ts","test/cli.test.ts:120:3"] without running test/cli.test.ts in the poolpooled root generated-state sentinelpassed542.15 ms
    routes ["-h"] without running test/cli.test.ts in the poolpooled root generated-state sentinelpassed414.97 ms
    routes ["--version"] without running test/cli.test.ts in the poolpooled root generated-state sentinelpassed428.24 ms
    routes ["--","--help"] without running test/cli.test.ts in the poolpooled root generated-state sentinelpassed437.21 ms
    routes ["--","-h"] without running test/cli.test.ts in the poolpooled root generated-state sentinelpassed518.06 ms
    routes ["--","--","--help"] without running test/cli.test.ts in the poolpooled root generated-state sentinelpassed515.84 ms
    records each pass of a split run writing ["--reporter=junit","--outputFile=report.xml"] as a blob report and merges thempooled root generated-state sentinelpassed578.11 ms
    records each pass of a split run writing ["test/bootstrap.test.ts","test/cli.test.ts","--reporter","html","-t","alpha"] as a blob report and merges thempooled root generated-state sentinelpassed615.24 ms
    refuses coverage in a split run and says no coverage provider is installedpooled root generated-state sentinelpassed515.37 ms
    refuses a split run writing the html report ["--reporter=html"] before any pass while @vitest/ui is missingpooled root generated-state sentinelpassed504.27 ms
    refuses a split run writing the html report ["--reporter","html","--outputFile=report/index.html"] before any pass while @vitest/ui is missingpooled root generated-state sentinelpassed506.81 ms
    refuses a split run writing the blob report ["--reporter","blob"], which Vitest cannot merge intopooled root generated-state sentinelpassed510.18 ms
    refuses a split run writing the blob report ["--reporter=blob","--outputFile=blob.json"], which Vitest cannot merge intopooled root generated-state sentinelpassed475.62 ms
    refuses a split run whose configuration sets reporters: ["blob"]pooled root generated-state sentinelpassed431.94 ms
    refuses a split run whose configuration sets coverage: { enabled: true }pooled root generated-state sentinelpassed473.04 ms
    refuses a split run whose configuration sets reporters: ["html"]pooled root generated-state sentinelpassed464.31 ms
    leaves a configuration Vitest cannot load to the passes, reading the caller's options alonepooled root generated-state sentinelpassed521.9 ms
    hands each pass the caller's environment, untouched by reading the configurationpooled root generated-state sentinelpassed557.4 ms
    leaves each pass its configured reporters when none of them writes a report filepooled root generated-state sentinelpassed559.44 ms
    forwards the listing command ["list","--filesOnly"] once, without running the CLI suitepooled root generated-state sentinelpassed436.72 ms
    forwards the listing command ["--reporter","verbose","list","test/cli.test.ts"] once, without running the CLI suitepooled root generated-state sentinelpassed437.08 ms
    forwards the benchmark command ["bench"] once, with no CLI unit pass and no contract treepooled root generated-state sentinelpassed445.66 ms
    forwards the benchmark command ["bench","test/cli.test.ts"] once, with no CLI unit pass and no contract treepooled root generated-state sentinelpassed478.95 ms
    forwards the benchmark command ["--","bench"] once, with no CLI unit pass and no contract treepooled root generated-state sentinelpassed454 ms
    keeps the dedicated CLI pass for the executing command watchpooled root generated-state sentinelpassed539.6 ms
    keeps the dedicated CLI pass for the executing command devpooled root generated-state sentinelpassed537.51 ms
    passes a pooled run that leaves repository-root generated state untouchedpooled root generated-state sentinelpassed444.56 ms
    runs ["-t","cli alpha"] as Vitest would select it, with the CLI suite only in its own passthe test wrapper over real Vitestpassed2358.05 ms
    runs ["--testNamePattern","gamma"] as Vitest would select it, with the CLI suite only in its own passthe test wrapper over real Vitestpassed2129.39 ms
    runs ["--","test/other.test.ts"] as Vitest would select it, with the CLI suite only in its own passthe test wrapper over real Vitestpassed1150.11 ms
    runs ["--","-t","beta"] as Vitest would select it, with the CLI suite only in its own passthe test wrapper over real Vitestpassed2091.28 ms
    runs ["--","--","test/other.test.ts"] as Vitest would select it, with the CLI suite only in its own passthe test wrapper over real Vitestpassed2308.33 ms
    writes one report holding both passes for ["--reporter","junit","--outputFile","report.xml"]the test wrapper over real Vitestpassed2583.74 ms
    writes one report holding both passes for ["--reporter=junit","--outputFile=report.xml"]the test wrapper over real Vitestpassed2225.96 ms
    writes one report holding both passes for ["--reporter=junit","--outputFile.junit=report.xml","-t","a"]the test wrapper over real Vitestpassed2675.32 ms
    writes one JUnit document to stdout when no output file is namedthe test wrapper over real Vitestpassed2514.93 ms
    merges both passes with the configuration file ["--config","custom.config.mjs"] they ran withthe test wrapper over real Vitestpassed2545.94 ms
    merges both passes with the configuration file ["-c","custom.config.mjs"] they ran withthe test wrapper over real Vitestpassed2479.93 ms
    merges both passes into the report file the configuration names with reporters: ["junit"], outputFile: "report.xml"the test wrapper over real Vitestpassed2722.9 ms
    merges both passes into the report file the configuration names with reporters: [["junit", { outputFile: "report.xml" }]]the test wrapper over real Vitestpassed2668.11 ms
    merges both passes into the report file a configuration names when it loads with the runner loader alonethe test wrapper over real Vitestpassed3027.04 ms
    merges both passes into the report file a configuration from a package names through its main filethe test wrapper over real Vitestpassed2327.72 ms
    merges both passes into the report file a configuration from a package names through its exports for import alonethe test wrapper over real Vitestpassed2570.49 ms
    merges both passes into the report file a configuration from a package names through its exports for import, where require names another filethe test wrapper over real Vitestpassed2619.16 ms
    reads a configuration that asks for a report file when process.env.NODE_ENV === "test" as the passes read itthe test wrapper over real Vitestpassed2437.87 ms
    reads a configuration that asks for a report file when process.env.VITEST === "true" as the passes read itthe test wrapper over real Vitestpassed2398.05 ms
    reads a configuration that asks for a report file when process.env.TEST === "true" as the passes read itthe test wrapper over real Vitestpassed2377.94 ms
    reads a configuration that asks for a report file when process.env.NODE_ENV === "production" as the passes read itthe test wrapper over real Vitestpassed2429.1 ms
    reports a failing pooled pass in the merged report and stops before the CLI passthe test wrapper over real Vitestpassed2081.83 ms
    prints usage for ["--","--help"] and runs no testthe test wrapper over real Vitestpassed585.73 ms
    prints usage for ["--","-h"] and runs no testthe test wrapper over real Vitestpassed464.05 ms
    derives the duplicate-ID verifier facts from the bound executable examplenot recordedpassed7299.21 ms
    executes the bound example against a fresh worldTypeScript and Markdown duplicates are both refused (spec:validation.duplicate-ids.dual-carrier)passed12.13 ms

Example space

  • firstCarrier · string · declared by a {firstCarrier} carrier declares {specId}
  • specId · string · declared by a {firstCarrier} carrier declares {specId}
  • secondCarrier · string · declared by a {secondCarrier} carrier declares {specId}

Bound points

ExamplefirstCarrierspecIdsecondCarrier
spec:validation.duplicate-ids.dual-carrier"TypeScript""spec:fixture.duplicate""Markdown"

Oracle binding: none

Coverage

firstCarrier: "TypeScript" · spec:validation.duplicate-ids.dual-carrier · not enumerable

specId: "spec:fixture.duplicate" · spec:validation.duplicate-ids.dual-carrier · not enumerable

secondCarrier: "Markdown" · spec:validation.duplicate-ids.dual-carrier · not enumerable

Unbound

  • firstCarrier: None
  • specId: None
  • secondCarrier: None
Design view

Requirement drawing

Requirement drawing for spec:validation.duplicate-ids
The diagram as text
  • spec:validation.duplicate-ids: Duplicate carrier IDs are excluded loudly · declared Spec
  • spec:validation.duplicate-ids.dual-carrier: TypeScript and Markdown duplicates are both refused · declared Spec
  • spec:validation.two-check-families: Validation separates well-formedness from non-pretending · declared Spec
  • impl:protocol.duplicate-id-exclusion: excludes duplicated carrier ids from the graph · anchored code · src/extract/index.ts:98
  • impl:protocol.duplicate-ids: rejects duplicate node identities from any graph producer · anchored code · src/validate/validators.ts:267
  • test:protocol.duplicate-ids.dual-carrier: dual-carrier duplicate-ID contract verifies carrier exclusion · anchored test binding · binding present · test/self-hosting-duplicate-ids.test.ts:1155
  • spec:validation.duplicate-ids refines spec:validation.two-check-families · declared — duplicate identity is a conformance refusal
  • spec:validation.duplicate-ids.dual-carrier refines spec:validation.duplicate-ids · declared
  • spec:validation.duplicate-ids.dual-carrier verifies spec:validation.duplicate-ids · declared
  • impl:protocol.duplicate-id-exclusion satisfies spec:validation.duplicate-ids · anchored
  • impl:protocol.duplicate-ids satisfies spec:validation.duplicate-ids · anchored
  • test:protocol.duplicate-ids.dual-carrier verifies spec:validation.duplicate-ids.dual-carrier · anchored
Mermaid source

Copyable source; the Studio draws static SVG.

Preferred and chosen syntax: requirementDiagram; exact refines, satisfies and verifies, with claim words in nodes and per-edge comments.

Decisions

No decision record named.

Runtime
  • component:protocol.extract · the carrier and source pipeline · src/extract/index.ts:197 · layer application · context protocol

    Uses

    • component:protocol.graph · the one graph and its semantic decisions · src/graph/schema.ts:34 · layer domain · context protocol
    • component:protocol.model · the authored contracts and stable ids · src/model/anchors.ts:100 · layer domain · context protocol
    • component:protocol.notation · the leaf step codec · src/notation/slots.ts:263 · layer domain · context protocol
    • component:protocol.validate · one finding currency and ordered checks · src/validate/validators.ts:76 · layer domain · context protocol
  • component:protocol.validate · one finding currency and ordered checks · src/validate/validators.ts:76 · layer domain · context protocol

    Uses

  • impl:protocol.duplicate-id-exclusion · excludes duplicated carrier ids from the graph · src/extract/index.ts:98

    Component: component:protocol.extract · the carrier and source pipeline · src/extract/index.ts:197 · layer application · context protocol

    Uses

    None.

  • impl:protocol.duplicate-ids · rejects duplicate node identities from any graph producer · src/validate/validators.ts:267

    Component: component:protocol.validate · one finding currency and ordered checks · src/validate/validators.ts:76 · layer domain · context protocol

    Uses

    None.

Runtime observation: not tracked.

Evidence

Runtime observation: not tracked.

Build attestations: not tracked.

Deployments: not tracked.

Bills of materials: not tracked.

Impact
Relations of spec:validation.duplicate-ids
  • E4: verifies · declared
The diagram as text
  • spec:carrier.markdown-parser: The Markdown parser reifies the ruled subset
  • spec:validation.duplicate-ids: Duplicate carrier IDs are excluded loudly
  • spec:validation.duplicate-ids.dual-carrier: TypeScript and Markdown duplicates are both refused
  • spec:validation.two-check-families: Validation separates well-formedness from non-pretending
  • spec:validation.duplicate-ids dependsOn spec:carrier.markdown-parser · declared — duplicate exclusion includes the default Markdown carrier
  • spec:validation.duplicate-ids refines spec:validation.two-check-families · declared — duplicate identity is a conformance refusal
  • spec:validation.duplicate-ids.dual-carrier refines spec:validation.duplicate-ids · declared
  • spec:validation.duplicate-ids.dual-carrier verifies spec:validation.duplicate-ids · declared
Mermaid source

Copyable source; the Studio draws static SVG.

Preferred syntax: flowchart. Chosen syntax: labelled flowchart, preserving exact relations, claims and folds.

Rests on

Rested on by

Distance 1

Relations

Relation and directionOther endClaimReasonExercised keys
dependsOn · outgoingThe Markdown parser reifies the ruled subset
spec:carrier.markdown-parser · resolves
declaredduplicate exclusion includes the default Markdown carrierNone
refines · outgoingValidation separates well-formedness from non-pretending
spec:validation.two-check-families · resolves
declaredduplicate identity is a conformance refusalNone
refines · incomingTypeScript and Markdown duplicates are both refused
spec:validation.duplicate-ids.dual-carrier · resolves
declaredNoneNone
verifies · incomingTypeScript and Markdown duplicates are both refused
spec:validation.duplicate-ids.dual-carrier · resolves
declaredNoneNone

Bindings

Implementation binding: present

  • impl:protocol.duplicate-id-exclusion · excludes duplicated carrier ids from the graph · src/extract/index.ts:98 · role validator · component component:protocol.extract · layer application · context protocol
  • impl:protocol.duplicate-ids · rejects duplicate node identities from any graph producer · src/validate/validators.ts:267 · role validator · component component:protocol.validate · layer domain · context protocol

Design reference: none

Verifier binding: present

Expected-outcome oracle: none

Runtime observation: not tracked.

Run results: not recorded in the graph.

Findings

None.

Composing (0)

Composing needs the script.