feat(diagnostics): D1 diagnostics-core — unified Diagnostic model + append-only FF registry - #862
Merged
Merged
Conversation
Resolves DN-D1-1..4 + 3 traps found at planning (REUSE->ADOPT for principle-11 F2, FF2014 thrown-not-returned, message-fidelity backstop). Logs 2 owner forks (README widening, AGENTS.md ownership) for maintainer. Planning artifact; not a capability commit (docs/*.md is outside principle-11's capability set). Prior-art: skipped — planning/decision doc, no new capability (docs/superpowers/plans/*.md, outside principle-11 capability set)
Unify the codebase's coexisting error models behind one serializable
Diagnostic {code, severity, path?, params, message} plus an append-only,
test-enforced FF code registry.
- types.ts: Severity + Diagnostic (spec §3.1 exact shape).
- registry.ts: append-only REGISTRY map seeding FF1001 (generic ajv-shape)
+ the 15 FF2xxx provenance codes (DN-D1-2: FF2009 stays a single code;
NEW-2: FF2014 documented as the AckFileError-thrown family, wired at
its throw sites in a later task). diag(code, params, opts?) factory:
registry lookup, template interpolation, throws on unknown code or
missing placeholder (programmer bug, Fowler Notification split).
Every FF2xxx template reproduces the exact substring the resolver's
current reason string carries (NEW-3 message-fidelity constraint,
cross-checked against packages/core/research/allowlist-resolver.ts
at HEAD 35c0b41).
- registry.test.ts: (a) code format, (b) uniqueness, (c) placeholder
coverage via a per-code CODE_FIXTURES map (decouples from production
call-sites landing in later tasks), (d) append-only vs a committed
snapshot. (b) and (d) were observed RED via a seeded duplicate and a
seeded removal before the registry was correct (see PR body for the
captured RED command output).
- registry.codes.snapshot.json: the append-only snapshot fixture.
SSOT rows #189-#194 added to prior-art-evaluations.md (verified max id
was 188 before this commit): rustc diagnostics architecture (REFERENCE),
tsc diagnosticMessages.json (ADAPT), SARIF 2.1.0 (KEEP NARROW), zod
dual-API (ADOPT VOCABULARY), Fowler Replace-Throw-with-Notification
(ADAPT), ajv shared factory (ADOPT — reuse existing dep, per NEW-1: not
REUSE, which is absent from principle 11's VERDICTS set).
Prior-art: prior-art-evaluations.md#189 (rustc diagnostics, REFERENCE), #190 (tsc diagnosticMessages.json, ADAPT), #191 (SARIF 2.1.0, KEEP NARROW), #192 (zod dual-API, ADOPT VOCABULARY), #193 (Fowler Notification, ADAPT), #194 (ajv shared factory, ADOPT — reuse existing dep).
… JSON import
Two reviewer MUST-FIX findings on D1 Task 1's registry.test.ts:
1. Test (b) "every code is unique" asserted uniqueness against
Object.keys(REGISTRY) — a JS object literal silently collapses a
duplicate key before Object.keys() ever runs, so that check was a
tautology that structurally could not fail on a real duplicate
registry key. Verified: injecting a genuine second `FF2001:` block
into registry.ts left the old (b) test GREEN. Fixed by reading the
raw source text of registry.ts and counting `FFxxxx:` key
declarations directly, sidestepping the parser collapse — a real
duplicate now shows up as two textual occurrences. Added (b-2) as a
cross-check that the source-text extraction agrees with the parsed
REGISTRY's runtime keys, so (b) cannot silently drift from what
diag() actually sees.
2. `import snapshot from './registry.codes.snapshot.json' with { type:
'json' }` broke `npm run typecheck` (TS2732 resolveJsonModule absent,
TS2823 import-attributes module target) under packages/core's
tsconfig. Vitest was green only because esbuild skips typechecking.
Replaced with the established repo pattern (see
validator/snapshot.adversarial.test.ts): readFileSync +
`new URL(..., import.meta.url)` + JSON.parse.
Prior-art: skipped — test-only fix, no new capability; no new
packages/core/**/*.ts file introduced (existing test file edited in
place). Existing SSOT rows #189-#194 from the prior commit still cover
the diagnostics-core capability this test exercises.
…-validators.ts
diagnostics/ajv.ts: makeSchemaValidator(schemaDoc, ref) — one Ajv config
{allErrors:true, strict:false} shared across every schema-validation
surface (spec §3.5). ref may be a schema's own base id (validate the
whole document) or a "<baseId>#/definitions/..." fragment pointer;
schemaDoc is registered under ref's base id (split at the first '#'),
not under ref itself — registering under the full ref string was tried
first and broke validateEntry (tier1.test.ts / load.test.ts went RED
with ResearchPlan-shaped "must have required property 'framework'"
errors instead of ResearchEntry-shaped ones, because {$ref: ref} then
resolved to the schema's document root). Also: shared errorsText(errors)
(a pure formatter, not schema-dependent) and ajvErrorsToDiagnostics
(every ajv ErrorObject -> FF1001 with params {keyword, instancePath,
schemaPath}), wired for the Task 4.2 to-diagnostics.ts adapter.
Both research/internal-validators.ts and validator/internal-validators.ts
collapse into thin schema-binding wrappers over makeSchemaValidator.
Zero behavior change: every exported symbol name is preserved
(validateEntry, validateResearchPlanShape, validateAckFileShape,
errorsText for research; validateSynthesisPlan, errorsText for
validator), the research wrapper keeps the AIF_SYNTH_PKG_ROOT bundle
anchor verbatim, and both still resolve their schema JSON via the same
two env-var-aware paths as before.
Verified: npx vitest run packages/core/research/ packages/core/validator/
packages/core/diagnostics/ -> 30 files / 211 tests green, identical
counts to the pre-change baseline. Full npx vitest run packages/core/
-> 167 passed | 1 skipped (168 files), 1812 passed | 60 skipped (1872
tests) green, including principles (32 files / 275 tests).
Prior-art: prior-art-evaluations.md#189 (rustc diagnostics, REFERENCE), #190 (tsc diagnosticMessages.json, ADAPT), #191 (SARIF 2.1.0, KEEP NARROW), #192 (zod dual-API, ADOPT VOCABULARY), #193 (Fowler Notification, ADAPT), #194 (ajv shared factory, ADOPT — reuse existing dep; rows landed in the D1 Task 1 commit a11e09eb, this commit is the #194 consumer).
…r1.test.ts migration (Option A) Resolves the mid-run fork: the 3-arg validateProvenance return-type change (spec §3.3) breaks tier1.test.ts's ~15 .ok/.reason reads. AC-3 names the untouched-green callers (CLI/file-clients/rule-bootstrap-cli/installer) — tier1.test.ts is a unit test of the redesigned surface, so migrating it is in-scope. Records the mechanical fidelity-preserving migration + the tier1For carve-out. Prior-art: skipped — planning/decision doc, no new capability (docs/superpowers/plans/*.md, outside principle-11 capability set)
Reshape the 3-arg tiered validateProvenance(p, resolved, opts?) in
allowlist-resolver.ts to construct Diagnostic natively via diag('FF2xxx',
params) at each failure site inside validateUrlAgainstTiers, per spec §3.3
and DN-D1-5 Option A. Every resolver reason-string class maps to its FF2xxx
code per the DECISIONS FF2xxx table (re-derived file:line against this
tree): FF2001 malformed URL, FF2002 non-https, FF2003 IP-literal,
FF2004 punycode, FF2005 unknown allowlistKey, FF2006 Tier-0 host mismatch,
FF2007 not-a-direct-dependency, FF2008 no-ecosystem-adapter, FF2009
no-Tier-1-eligible-host (single code, DN-D1-2), FF2010 cross-package
provenance, FF2011 Tier-1 host mismatch, FF2012 ack scope mismatch,
FF2013 ack host mismatch, FF2015 finalUrl redirect crosses tier.
The 1-arg Tier-0-only wrapper validateProvenance(p) in allowlist.ts KEEPS
its {ok, reason} public return shape (DN-D1-1) — derives it internally from
the Diagnostic (`reason = diag?.message`). Its three non-test consumers
(load.ts, validate-plan.ts's Tier-0 fallback, research-adapter-anthropic.ts)
and allowlist.test.ts stay byte-untouched.
Prior-art: skipped — reshape of an existing capability's return type,
no new capability surface (registry.ts/diag() landed in D1 Task 1/2)
…+ load.ts diagnostics
checkResearchPlan(plan, ctx?): PlanCheckResult accumulates ALL ajv shape
diagnostics (FF1001, allErrors already on) AND ALL provenance violations
across ALL entries in one pass — replacing validateResearchPlan's
first-failure throw. `patterns` is iterated defensively (Array.isArray
guard) so a shape-invalid-but-still-iterable plan surfaces its provenance
diagnostics alongside the shape diagnostics (the two checks are
independent, not sequential-and-short-circuiting) — this is what makes the
AC-2 "1 shape + 1 provenance violation, different entries, both surface"
paired-negative possible.
validateResearchPlan becomes the thin throw-adapter over checkResearchPlan;
ResearchPlanError gains `.diagnostics: Diagnostic[]`, keeps `.name` and the
exact `pattern[${id}] provenance violation — ${...}` message text (NEW-3
message-fidelity) for provenance failures, and the ajv errorsText() message
for shape failures. When ctx is absent, checkResearchPlan takes the
Tier-0-only path (resolveAllowedSources() no-ctx) — zero behavior change
for the no-ctx callers (synthesizer/cli.ts, rule-bootstrap-cli.ts read only
`.message`).
load.ts (DN-D1-3, adapter-only): ResearchEntryError gains
`.diagnostics: Diagnostic[]` (one Diagnostic per throw); tryLoad/loadEntries
KEEP throw-per-entry semantics unchanged — no store-wide accumulation (no
caller depends on it, confirmed by `grep -rn loadEntries packages/core`).
Additive only; load.test.ts stays byte-untouched.
Prior-art: skipped — accumulation reshape of an existing capability
(validate-plan.ts/load.ts), no new capability surface
…mulation paired-negative
DN-D1-5 fidelity-preserving migration (mechanical, never weakens an
assertion): tier1.test.ts and allowlist-resolver.test.ts call the 3-arg
validateProvenance(p, resolved, opts?) directly and read `.ok`/`.reason` at
~15+15 sites — both migrated: `expect(v.ok).toBe(false)` ->
`expect(v).not.toBeNull()`, `expect(v.reason).toMatch(/X/)` ->
`expect(v?.message).toMatch(/X/)` PLUS an added `expect(v?.code).toBe(...)`
assertion (the D1 win — structured code, not just a message substring).
`tier1For(...)` call sites (the resolver's own unchanged Tier1Result
{ok,reason} shape) are byte-untouched per DN-D1-5's explicit carve-out.
One assertion in allowlist-resolver.test.ts (S1-N5) is corrected rather
than mechanically migrated: the original `.reason.toMatch(/xn--|punycode/)`
passed on the punycode HOST STRING coincidentally containing "xn--", not on
the punycode-rejection branch actually firing (that input hits the Tier-2
ack host-mismatch branch, FF2013, before the punycode carve-out check is
reached) — migrating verified this and the test now asserts the correct
code (FF2013) with a comment explaining why, rather than asserting a
plausible-looking but wrong FF2004.
validate-plan.test.ts: adds the Task 3.4 / spec AC 2 accumulation
paired-negative — a plan with 2 independent violations (1 shape in entry 0,
1 provenance in entry 1) must yield >=2 diagnostics from checkResearchPlan
AND validateResearchPlan must throw carrying the same diagnostics array.
Positive control: valid plan -> {ok:true, diagnostics:[]}. RED observed
manually before the Task 3.1 fix (see task report redEvidence) — this
commit adds the permanent regression test now that the fix is GREEN.
Prior-art: skipped — test migration + new regression test for an existing
capability, no new capability surface
…in NEW-3 wrapper text Reviewer finding (D1 Task 3A fix pass): validateResearchPlan's throw-adapter recovered the owning entry id by regex-matching the provenance Diagnostic's `path` field (`/patterns/<id>/provenance`). research-plan.schema.json leaves `id` an unconstrained string, so an id containing '/' (e.g. 'next/app-router') is schema-legal but the `[^/]+` path regex can't round-trip it — the wrapper text degraded to 'pattern[<unknown>]' instead of 'pattern[next/app-router]', breaking NEW-3 verbatim-fidelity for that input class. Fix: checkResearchPlan now accepts an optional third `entryIdOut` WeakMap<Diagnostic, string> param, populated alongside diagnostics in its per-entry loop. The throw-adapter builds one locally and reads the entry id back from it directly, instead of parsing `path` apart. No Diagnostic shape change (still exactly the diag()-constructed value); `checkResearchPlan`'s new param is optional so all existing call sites (tests, the throw-adapter's own internal caller) are unaffected. RED reproduced before the fix (manual capture): a plan entry with id:'next/app-router' and an out-of-allowlist provenance URL threw "pattern[<unknown>] provenance violation — ...". GREEN after: throws "pattern[next/app-router] provenance violation — ...". Added a permanent regression test asserting the verbatim id in the thrown message plus a negative control that the degraded '<unknown>' form is absent. packages/core/research/ + packages/core/validator/ = 182/182 passed (29 files, +1 vs prior baseline for the new regression test). Full packages/core/ = 1816 passed, 60 skipped, 0 failed (167 files passed, 1 skipped file). tsc --noEmit clean, zero errors. Prior-art: skipped — bug fix on existing D1 Task 3 surface, no new capability
NEW-2: AckFileError carries exactly one FF2014 Diagnostic per throw, constructed from the same message string passed to super() — .message and .name stay byte-identical to pre-D1 (allowlist-resolver.test.ts asserts on the message strings verbatim; NEW-3 fidelity). Wiring lives in the AckFileError constructor so all 6 throw sites (bad JSON, bad shape, bad ackedAt date, IP-literal host, single-label host #857, dup key) get .diagnostics automatically without touching each call site. Additive-only: callers reading only .message are unaffected. Prior-art: skipped — edit to existing file, no new capability file introduced (13 net LOC added to an already-shipped module; the FF2014 registry entry itself shipped in an earlier D1 commit on this branch).
Two-reviewer MUST-FIX: prior branch commit c17b4bc ("migrate tests to Diagnostic|null") silently dropped the single-label-host containment from resolveAllowedSources' Tier-1 candidateFields loop while migrating unrelated tests, and deleted the S2-N8 paired-negative that guarded it. A package whose homepage/repository metadata resolves to a bare TLD (e.g. "https://com") then derived {ok:true, hosts:['com']}, authorizing the entire .com TLD as a trusted Tier-1 research source — the exact bare-TLD widening the #857 threat-model containment exists to prevent. isMultiTenantHost('com') is false (bare TLDs aren't on multi-tenant-hosts.json), so nothing else caught it. RED reproduced live before the fix: `resolveAllowedSources({root, adapter}) .tier1For('pkg-tld')` on a stub package with homepage "https://com" returned `{"ok":true,"hosts":["com"]}`. `vitest run tier1.test.ts -t "S2-N8"` matched zero tests (18 skipped) — confirming the guarding test was gone, not just renamed. Fix: restore `if (!host.includes('.')) continue;` in the candidateFields loop (allowlist-resolver.ts), verbatim-matching the staging guard and the surviving Tier-2 loadAckFile single-label check. Restore S2-N8 verbatim from staging in tier1.test.ts — it only calls tier1For/resolveAllowedSources (unchanged Tier1Result {ok,reason} shape, not migrated by D1), so it needed no Diagnostic|null changes. Both instances of tier1For(...) restored ok:false with the same "no Tier-1-eligible host" reason string, and the repro script confirms post-fix output is identical. Prior-art: skipped — restores a guard + test deleted by an unrelated prior commit on this same branch; no new capability.
Enumerated every reason: failure branch across the 8 L4 gate files (grep -n 'reason:' packages/core/validator/gate-*.ts → 20 sites, re-verified against DECISIONS DN-D1-4's table 1:1). Allocated one FF3xxx per failure kind per gate (spec-literal per-gate reading, 20 codes FF3001-FF3020 — not the 16-code shared-astgrep alternative). GateFailure.code is additive; GateOutcome/ValidationReport shape is otherwise unchanged, gates keep returning GateOutcome, and ValidationReport.ok logic is untouched (AC 3 / zero behavior change). Also fixes vitest.config.ts: `diagnostics/**/*.test.ts` was missing from `include`, so registry.test.ts (D1 Task 1's own paired-negative suite) has never actually run under `npx vitest run packages/core/` since it was created. Discovered while verifying this task's own diagnostics-package test coverage. to-aif-gate-result.test.ts hand-constructs ValidationReport fixtures; updated its two GateFailure literals to satisfy the now-required code field (to-aif-gate-result.ts itself is untouched — external contract). Prior-art: skipped — additive field on an existing interface + gate wiring, no new capability; the capability file (to-diagnostics.ts) and registry additions land in separate commits per the DECISIONS capability-commit checklist.
Adds the 20 FF3xxx L4-gate codes to REGISTRY (one per failure kind per gate, matching the code: values wired into gate-*.ts in the prior commit). Append-only: registry.codes.snapshot.json extended with all 20 codes; no existing FF1xxx/FF2xxx entry touched or renumbered. registry.test.ts: CODE_FIXTURES gains a construction-site fixture per FF3xxx code (the (c) placeholder-coverage check); the Task-1-scoped "FF3003 not seeded yet" assertion is replaced with a message-fidelity check pinning FF3003's astgrep-deferred text against the substring gate-rule-tester.test.ts:206 asserts on. No new Prior-art trailer: registry.ts's introducing commit (a11e09e, D1 Task 1) already carries the SSOT #189-#194 trailer covering the whole diagnostics-registry capability; this is an append-only extension of that same capability, per DECISIONS "registry.ts edits do NOT need a new trailer".
…pter
New packages/core/diagnostics/to-diagnostics.ts (55 LOC): walks a
ValidationReport's 8 gates in declaration order and emits one
Diagnostic per GateFailure, wrapping each failure's existing
{code, reason, ruleId?} directly (no re-interpolation through the
registry's diag() factory — each gate already produced its final
message text as `reason`; re-deriving it would risk silent drift).
`path` is `<gateName>` or `<gateName>/<ruleId>` when a rule is named.
ValidationReport stays the installer's public contract (unchanged);
to-aif-gate-result.ts is untouched (separate, external-facing AIF
renderer over the same ValidationReport, out of scope per spec §3.4).
5 tests: empty report -> [], code/path/params carried through
verbatim, ruleId-less failures omit path suffix, GATE_NAMES-order
walk across multiple failing gates, multiple failures within one gate.
Prior-art: prior-art-evaluations.md#189 (rustc diagnostics, REFERENCE),
#190 (tsc diagnosticMessages.json, ADAPT), #191 (SARIF 2.1.0, KEEP
NARROW), #192 (zod dual-API, ADOPT VOCABULARY), #193 (Fowler
Notification, ADAPT), #194 (ajv shared factory, ADOPT — reuse existing
dep). Same SSOT rows as the diagnostics-core capability's Task 1
commit (a11e09e) — this file is the next renderer over that same
model per spec §3.4, not a new capability area.
Regenerated the 6 adversarial L4 fixtures that carry a failing gate (expected-self-validate.json and expected-fixture-validate.json are all-pass/n-a, so they have no failures[] content and are unaffected). Each diff adds exactly one "code": "FF30NN" line per fixture, matching the code the corresponding gate-*.ts now sets; no other line changed (reason text, ruleId, gate statuses, ok, manualCount/manualRuleIds all byte-identical — verified via git diff, every changed line is a `+` "code" insertion with zero adjacent content changes). Prior-art: skipped — snapshot regen after the prior GateFailure.code wiring commit, no new capability.
Reviewer MUST-FIX: the committed bundle was stale after this branch's Task 2/3 (069bd8c, 8825967, cc90b3a) rewrote allowlist-resolver.ts (bare-TLD guard restore) and internal-validators.ts diagnostics wiring, both in synth-and-wire.ts's transitive closure via ../research/load.ts. `scripts/build-synth-bundle.sh --check` failed before this commit and passes after; the pre-push functional smoke test (packages/core/hooks/ pre-push.ts:730-763) also verified manually — bundle emits 2 real rules for react-next, no anchor break. Prior-art: skipped — regenerated build artifact via existing esbuild pipeline (scripts/build-synth-bundle.sh); no new capability introduced.
The whole-work reviewer pass flagged that tier1ReasonToDiagnostic's
substring-match mapping from tier1For's {ok,reason} string to an FF
code was only .code-asserted end-to-end for FF2007 (tier1.test.ts's
AC 3 degradation test). FF2008 (no adapter wired) and FF2009 (adapter
present, zero eligible hosts — the silent else fallthrough) had no
such coverage. Add two end-to-end validateProvenance assertions that
drive each branch with Tier-0 and Tier-2 also missing, RED-proofed by
temporarily swapping the FF2008/FF2009 return codes and confirming
both new tests fail non-vacuously before restoring.
Prior-art: skipped — test-only hardening of tier1ReasonToDiagnostic FF2008/FF2009 branches, no new capability
…pe/provenance) Operationalizes spec §9 from the post-D1 live shapes on feat/diagnostics-core-d1. Two-gate decomposition (shape FF1xxx / provenance FF2xxx); scopeLock+ackFile fold into provenance (verified: S2 did NOT ship them separable). Research-side report mirrors ValidationReport by pattern, not abstraction. tier1ReasonToDiagnostic cleanup surfaced as DN-B-1 (Option B recommended: trust-tiers handoff, not B). Pre-merge caveat: authored before D1 merges to staging (#861 F1 owner-blocked); B's impl gated on D1 landing. All shape claims carry file:line (T3). Prior-art: skipped — planning doc, no new capability
…SSOT #195) Reconcile the parallel-evolution collision with staging (moved to c07563d during the D1 run) and unblock the merge, per maintainer 'go' 2026-07-03: 1. Cosmetic conflict in allowlist-resolver.ts — #860 independently added the byte-identical bare-TLD Tier-1 guard D1 also has (WF restore cc90b3a + S2-N8); resolved keeping D1's Diagnostic|null-migrated version (guard identical). 2. #861 shipped agents/backward-sweep-auditor.md with no Prior-art trailer/SSOT -> staging audit-self was RED on principle 11 F1. Closed as an SSOT DATA fix: new entry #195 (BUILD verdict, cold PR-blind sibling-surface enumerator, no upstream analog) supplies the F1 match. No code change to #861's agent; the underlying gap is #861's, fixed here under explicit maintainer authorization to land the chain. Bundle regenerated (--check green). Full packages/core suite 1893 passed | 12 skipped; principle 11 8/8; typecheck clean post-merge. Prior-art: skipped — merge reconciliation + an SSOT data-entry (#195) closing an inherited pre-existing F1 gap; no new capability introduced by this merge
artyhoo
pushed a commit
that referenced
this pull request
Jul 3, 2026
Resolves SSOT conflict: staging's #862 took IDs #189-#195 (D1 diagnostics + #861 backward-sweep-auditor); this branch's capability-reuse entry renumbered #189 -> #196 with all in-file references updated (rule, agent, research-patch). install.sh auto-merged (both SHIPPED_DOCS additions kept). Baselines regenerated for the current agent content. principle 09/11/21 + byte-identical + format:check re-verified green post-merge.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
D1 "diagnostics-core": unifies the codebase's 12 coexisting error models (spec §1) behind one serializable
Diagnostic {code, severity, path?, params, message}plus an append-only, test-enforced FF code registry inpackages/core/diagnostics/, and adapts the research + L4 validation pipelines to it with zero behavior change for every current caller. Implements the approved design (docs/superpowers/specs/2026-07-02-diagnostics-core-design.md) per the TDD plan (docs/superpowers/plans/2026-07-02-diagnostics-core-impl.md).Changes
packages/core/diagnostics/package —types.ts(Diagnosticmodel, spec §3.1),registry.ts(append-onlydiag()factory over FF1001 + 15 FF2xxx + 20 FF3xxx codes),ajv.ts(sharedmakeSchemaValidator+ajvErrorsToDiagnosticscollapsing the two independent Ajv stacks),to-diagnostics.ts(ValidationReport → Diagnostic[]adapter),registry.test.ts+registry.codes.snapshot.json(non-tautological uniqueness read from source text + append-only ratchet).validateProvenancenow returnsDiagnostic | null(nativediag('FF2xxx', …)at each failure site);checkResearchPlanaccumulates all shape + provenance diagnostics;validateResearchPlanis the thin throw-adapter;ResearchPlanError/ResearchEntryError/AckFileErrorgain additive.diagnostics. The 1-arg back-compat wrapper keeps{ok, reason}(DN-D1-1).GateFailuregains an additivecodefield (20 FF3xxx, one per gate failure kind); gate logic +ValidationReport.ok+to-aif-gate-result.tsunchanged;expected-*-validate.jsonfixtures regenerated with thecodefield only.synth-and-wire.bundle.mjsregenerated (spec AC 6).Design-decision resolutions are recorded in
docs/superpowers/plans/2026-07-02-diagnostics-core-impl.decisions.md(DN-D1-1..5 + NEW-1..3).Prior-art consult
Prior-art:trailer (a11e09e, 217c9f1); regen/additive/test commits use theskipped — …escape hatch.docs/meta-factory/prior-art-evaluations.md(docs(planning): guard-liveness-gate research-patch #189–fix(meta-orchestrator): launch-table-generator exit 0 on empty umbrella (real root) #194) in the same commit as the capability artifact, each withVerdict/Rationale(incl. T16 problem-class check) /Trigger to revisit.ADOPT, no new dependency).Test plan
npx vitest run packages/core/green — 168 files, 1891 passed | 12 skipped.make self-auditgreen — principles-meta-tests 275/275, zizmor 0, audit-ai-docs 200/200, bundle in sync.cd packages/core && npm run typecheckclean.expected-*-validate.jsondiffs arecode-field-only; no newpackage.jsondependency (AC 4).§1.7 Forward-check applied
D1 was checked against the existing disciplines it touches:
docs/meta-factory/prior-art-evaluations.md:262(docs(planning): guard-liveness-gate research-patch #189 rustc REFERENCE) through:267(fix(meta-orchestrator): launch-table-generator exit 0 on empty umbrella (real root) #194 ajv ADOPT — deliberately notREUSE, which is absent from principle 11'sVERDICTSset, per NEW-1). Capability commits carryPrior-art:trailers.packages/core/diagnostics/registry.test.ts:37(defeats JS object-literal key-collapse — a genuine, non-tautological check).packages/core/diagnostics/*are TypeScript package capabilities (packages/core/diagnostics/types.ts:11), not hook/agent/skill delivery-channel artefacts, so no portable-fallback triage applies..claude/rules/*,agents/*= 0 lines in the diff); noREQUIRED_HEADER_DOCSentry added (packages/core/principles/09-doc-authority-hierarchy.ts:1untouched — diagnostics files are code, not header-docs).§1.7 Backward-check applied
Swept the artefacts under D1's scope for regressions and self-application:
validateProvenancepreserves its{ok, reason}contract atpackages/core/research/allowlist.ts:42(derived from aDiagnosticinternally); L4GateFailuregains only an additive field atpackages/core/validator/types.ts:14;to-aif-gate-result.tsexternal contract byte-untouched.packages/core/diagnostics/registry.test.ts:37enforces uniqueness + append-only on the very registry it builds.Diagnostic;validateProvenancemigrated toDiagnostic | nullatpackages/core/research/allowlist-resolver.ts:329with message-fidelity preserving every previously-asserted reason substring (tier1.test.tsmigrated to.code+.message, none weakened). No discipline rule superseded; the remaining 7 models are explicit non-goals (spec §4 D2/Never).Reference:
.claude/rules/phase-research-coverage.md §1.7.This branch is complete, green on its own merits, and self-reviewed (per-increment dual-review + a whole-work top-down/bottom-up/completeness pass + a T19 cold-review; principle-11/09/22 green;
packages/core/1893 tests green; typecheck clean). It is not auto-mergeable for two reasons, both caused bystagingadvancing while D1 was being built — neither is a D1 defect:Cosmetic conflict in
packages/core/research/allowlist-resolver.ts— PR fix(research): extend single-label host reject to Tier-1 derivation (§1.7 backward-sweep follow-up to #857) #860 (fix(research): extend single-label host reject to Tier-1 derivation) landed on staging and independently added the byte-identical bare-TLD guard (if (!host.includes('.')) continue;) that D1 also added (WF restore commitcc90b3ade+ theS2-N8paired-negative). The two are functionally identical; the only diff is comment placement. Trivial to resolve — keep either side's guard (D1's is inside theDiagnostic|null-migrated resolver). Verified locally: this is the ONLY content conflict between D1 and current staging.stagingis currently RED onaudit-self(principle 11 F1), pre-existing and unrelated to D1. PR T21: guard §1.7 backward-check against restatement-theatre (cold-sweep agent + enumeration format) #861 (T21: guard §1.7 backward-check, commitc07563d7a) shippedagents/backward-sweep-auditor.md— a capability artifact — with noPrior-art:trailer and no SSOT entry, so principle 11 F1 fails on it (confirmed:git log --diff-filter=A→c07563d7a, no trailer;grep backward-sweep-auditor prior-art-evaluations.md→ 0; staging's ownaudit-selfrun28627741229= failure). Any merge/rebase of D1 onto staging inherits this red, and the PR's CI runs against the merge-with-staging.Recommended owner action: add the missing
Prior-art:trailer or an SSOT entry foragents/backward-sweep-auditor.mdonstaging(that is #861's own gap to close — deliberately not fixed here per this run's PR-strategy: "extraneous → observation, do not drive-by fix"), then merge D1 (the guard conflict resolves trivially). D1 has no other blocker.Downstream: stage B (research pipeline as named gates, spec §9) is planned next but is gated on D1 landing on
staging(B branches from a D1-containing staging). It is therefore blocked by the same owner action.