testguard-cli 0.15.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +196 -17
  3. package/package.json +1 -1
  4. package/spec/GATE-SEMANTICS.md +316 -0
  5. package/spec/README.md +27 -2
  6. package/spec/lib/authoring-input.mjs +40 -0
  7. package/spec/lib/origins.mjs +56 -0
  8. package/spec/lib/validate.mjs +129 -1
  9. package/spec/schemas/annotations.schema.json +65 -0
  10. package/spec/schemas/authoring-input.schema.json +67 -0
  11. package/spec/schemas/brief.schema.json +2 -0
  12. package/spec/schemas/claims.schema.json +8 -8
  13. package/spec/schemas/common.schema.json +73 -0
  14. package/spec/schemas/evidence.schema.json +41 -8
  15. package/spec/schemas/status.schema.json +31 -2
  16. package/src/brief/brief.mjs +11 -4
  17. package/src/claims/annotation-placement.mjs +357 -0
  18. package/src/claims/annotations.mjs +61 -2
  19. package/src/cli.mjs +40 -5
  20. package/src/commands/admit.mjs +21 -3
  21. package/src/commands/annotations.mjs +67 -0
  22. package/src/commands/claims.mjs +46 -6
  23. package/src/commands/probe.mjs +50 -7
  24. package/src/commands/replay.mjs +7 -0
  25. package/src/commands/scaffold.mjs +49 -2
  26. package/src/commands/sweep.mjs +7 -0
  27. package/src/evidence/writer.mjs +12 -4
  28. package/src/gate/changed.mjs +5 -1
  29. package/src/git.mjs +4 -4
  30. package/src/init/init.mjs +6 -0
  31. package/src/init/templates/SKILL.md +78 -0
  32. package/src/probe/attribution.mjs +19 -0
  33. package/src/probe/contention.mjs +20 -2
  34. package/src/probe/cost.mjs +22 -9
  35. package/src/probe/inject.mjs +2 -6
  36. package/src/probe/origin-policy.mjs +69 -0
  37. package/src/probe/policy-freshness.mjs +74 -0
  38. package/src/probe/probe.mjs +74 -45
  39. package/src/probe/runners/discovery.mjs +2 -0
  40. package/src/probe/runners/jest.mjs +2 -2
  41. package/src/probe/runners/lifecycle.mjs +58 -0
  42. package/src/probe/runners/playwright.mjs +6 -2
  43. package/src/probe/runners/python.mjs +10 -4
  44. package/src/probe/runners/shared.mjs +12 -6
  45. package/src/probe/runners/vitest.mjs +3 -3
  46. package/src/probe/universe.mjs +47 -0
  47. package/src/probe/worktree.mjs +3 -1
  48. package/src/render.mjs +17 -1
  49. package/src/replay/replay.mjs +8 -4
  50. package/src/scaffold/admission.mjs +117 -0
  51. package/src/scaffold/append.mjs +41 -0
  52. package/src/scaffold/apply-append.mjs +71 -0
  53. package/src/scaffold/fix-input.mjs +119 -0
  54. package/src/scaffold/fix-paths.mjs +90 -0
  55. package/src/scaffold/intent.mjs +2 -0
  56. package/src/scaffold/open-append.mjs +44 -0
  57. package/src/scaffold/plan-append.mjs +37 -0
  58. package/src/scaffold/plan-intent-input.mjs +38 -0
  59. package/src/scaffold/prepare-append.mjs +46 -0
  60. package/src/scaffold/request.mjs +30 -0
  61. package/src/scaffold/scaffold.mjs +23 -10
  62. package/src/status/status.mjs +42 -10
  63. package/src/sweep/pool.mjs +33 -0
  64. package/src/sweep/sweep.mjs +9 -4
@@ -1,5 +1,129 @@
1
1
  # Gate semantics
2
2
 
3
+ ## Declared claim origins
4
+
5
+ Status rechecks every recorded automatic-discovery dependency hash, including
6
+ negative candidate tests, barrels and resolver configuration. Edited, deleted,
7
+ unreadable or non-regular/escaping inputs make the evidence stale without
8
+ rewriting its verdicts. Legacy omission remains readable but does not establish
9
+ a complete origin-policy freshness binding. Rechecking recorded dependencies
10
+ alone does not detect a newly added test/configuration: native test-universe
11
+ recomputation remains required before origin-policy activation.
12
+
13
+ Canonical and recorded claim sources share the closed kinds `spec`, `adr`,
14
+ `annotation`, `comment`, `manual`, `doc`, `bug`, `incident`, `review`, `inferred`.
15
+ These are declarations, not authentication of independence, chronology,
16
+ production occurrence or correctness. The optional reference remains opaque
17
+ metadata (at most 512 characters); verification does not fetch it. Producer
18
+ provenance and measured test independence are separate signals.
19
+ Adding kinds does not change verdicts, baseline/fingerprint identity or default
20
+ gates. Existing ranking weights stay unchanged; new kinds use the existing 0.9
21
+ fallback, not an invented strength hierarchy. Existing documents remain valid
22
+ under the candidate reader; older closed-schema readers reject new kinds.
23
+ Optional `origins` summaries on evidence, status and brief have closed kind
24
+ buckets and safe nonnegative integer counts. Distinct claim buckets plus
25
+ `mixed` equal the claim total; record buckets equal the record total. Status
26
+ uses `basis: declared`, omits records, has zero mixed claims and binds its
27
+ total to current `counts.claims`, even when evidence is stale. Evidence and
28
+ brief use `basis: recorded` and require record counts. Evidence validation
29
+ recomputes from every actual record regardless of verdict: each claim ID
30
+ counts once, with conflicting source kind OR reference counted as one mixed
31
+ claim. Object-key order is not a conflict; absent and empty references differ.
32
+ Brief totals bind to the complete summary, not its capped/filtered items.
33
+ Status and brief omit the source projections needed to independently validate
34
+ their kind split; arithmetic validity is not source authentication. Legacy
35
+ documents without summaries remain valid; old closed readers reject them.
36
+ Status supplies current declared origins once validated claims are available,
37
+ including empty, unprobed, invalid-anchor and stale-evidence states. Probe
38
+ evidence and evidence-backed briefs supply recorded origins from all records,
39
+ including killed and non-killed records, before caps or baseline/floor filtering.
40
+ Human status/probe/brief text and Markdown label the basis and print "declared
41
+ origins, not authenticated independence". Evidence-free briefs and projects
42
+ without a claims file omit the summary rather than inventing declarations.
43
+ Probe JSON retains status's current declared basis; saved evidence has its own
44
+ recorded basis. These informational fields do not change states, next actions,
45
+ exit codes, fingerprints or default gates. Opt-in origin policy remains pending.
46
+
47
+ ## Explicit annotation authoring
48
+
49
+ Ordinary `claims` reports a separate `annotationAdvisory` with distinct sorted
50
+ `missingIds` and informational `notes`; status uses its existing informational
51
+ `notes`. Missing links reduce discoverability and annotation reconciliation,
52
+ not exact-anchor validation. Advice supplies `testguard claims --annotate` for
53
+ read-only preview; it never changes state, next verification action, verdicts,
54
+ source kinds or exit codes. Annotation-sourced missing IDs still retain the
55
+ existing claims drift error. Scanned occurrences are lexical links, not proof
56
+ of intent, source ownership or authenticated provenance.
57
+ Status advisory scanning is bounded to 10,000 directory entries, 1,000 files,
58
+ 64 directory levels, 256 KiB per file and 2 MiB total read bytes. Read failures,
59
+ unsafe file identities or limits produce unavailable advice, never a claim
60
+ that an ID is absent from an incomplete scan. Verification state/next still
61
+ comes from its existing inputs. For custom claims, preserve the original
62
+ `--claims` argument when following the preview suggestion.
63
+ Scanner and placement ID prefixes use mandatory separators between repeated
64
+ alphanumeric groups, avoiding overlapping quantifiers on tokens lacking a
65
+ hyphen without changing the supported identifier vocabulary.
66
+
67
+ `claims --annotate` previews file-header placement without writes. Add `--apply`
68
+ to explicitly apply a newly computed plan; a previous invocation's preview is
69
+ not a saved approval token. Repeated/comma-separated `--claim` selects IDs;
70
+ unknown or empty IDs are usage errors, never an implicit all-claims selection.
71
+ Authoring flags are valid only on `claims`; `--apply` requires `--annotate`.
72
+ Authoring rejects unrelated inspection/output switches and writes results only
73
+ to stdout. It never updates claims metadata, evidence or baselines.
74
+
75
+ The closed `annotations` document describes authoring, not verification. It
76
+ contains selected IDs, admitted target/action summaries, refusals and, for
77
+ apply, touched/verified/unchanged files and explicit failure/recovery details.
78
+ It excludes source/proposed contents, inode identities, hashes and absolute
79
+ source/root paths. Preview exits 0 when applicable, 2 when refused. Apply exits
80
+ 0 only for a fully verified apply (including idempotent no-op), 2 for refusal or
81
+ partial failure. Invalid invocation exits 3. No authoring result changes probe
82
+ verdicts, rank, coverage, source-kind declarations or authentication of intent.
83
+ Failure reasons are stage-specific public summaries: raw syscall details can
84
+ contain absolute input paths and remain private, never passed through to JSON.
85
+
86
+ Placement supports JavaScript/TypeScript and Python file-level line-comment
87
+ headers, not symbol ownership. Exact anchors remain checked without annotations.
88
+ Every selected target must pass conservative path, regular-file, UTF-8/newline,
89
+ anchor and size admission: 2 MiB per original/proposed file and 16 MiB aggregate
90
+ charged before reads plus proposal growth. Any refusal blocks all writes.
91
+ Preserve BOM, shebang, Python encoding cookies, newline style, inode and mode.
92
+ Only leading line-comment headers are placement witnesses, not string contents.
93
+
94
+ Apply reloads actual claims under an exclusive owned lock, replays all targets,
95
+ and fsyncs owner-only originals/manifest outside the canonical project before
96
+ writing. Recheck claims and targets immediately before each non-truncating
97
+ descriptor write; verify output, distinguish potentially touched from verified
98
+ files, and retain private recovery on partial failure. Reject the actual claims
99
+ file as a target, root/descendant symlinks and recovery inside the checkout.
100
+ Resolve parent aliases to bind the actual root. Never reclaim another writer's
101
+ lock or automatically roll back over an editor's changes. Stage and fsync the
102
+ final recovery journal before final naming. This is recoverable, not atomic:
103
+ arbitrary same-inode editors and process/power failure can race or interrupt
104
+ writes; directory durability across power loss is not guaranteed.
105
+
106
+ ## Fault-specific defender selection
107
+
108
+ A fault's `defendedBy`, when present, overrides its claim's declaration.
109
+ An explicit empty fault list requests discovery and never falls back to the
110
+ claim. Without a fault override, a non-empty claim list is inherited; otherwise
111
+ defenders are discovered. New evidence records `defenders.selectionSource` as
112
+ `fault`, `claim`, or `discovery`, alongside the actual requested and resolved
113
+ sets. Empty requests use discovery and carry `discovered: true`, including an
114
+ explicit empty fault override. Non-empty requests are never marked discovered.
115
+ Dirty-input preflight, runner ownership, mock-awareness, input hashing and
116
+ baseline caching all operate on each fault's selected defender set.
117
+
118
+ Changing requested globs, resolved files or selection origin invalidates verdict
119
+ reuse. Legacy evidence remains readable but cannot authorize a new fault-level
120
+ override. A strict resolved subset after a previous kill is reported by `claims`
121
+ as a narrowing warning and requires re-probing; it is not a new verdict.
122
+ Cost reports include per-fault recorded run durations and selection origin.
123
+ Shared defenders identify the actual fault records that selected each file,
124
+ including multiple faults within one claim. File costs remain overlapping upper
125
+ bounds, not additive shares. Existing total/per-claim budget ceilings are unchanged.
126
+
3
127
  How a Guard-spec tool decides whether CI goes red. Shared by every tool that
4
128
  adopts the spec; each tool may add stricter rules, never looser ones.
5
129
 
@@ -87,6 +211,17 @@ Rules that follow from the table:
87
211
  the fault is probed again: the cost of re-measuring is a run, the cost of
88
212
  the other direction is a verdict nobody measured.
89
213
 
214
+ **Claim metadata is also an input to reuse.** The recorded claim id,
215
+ statement, severity, source (including its reference), and optional
216
+ producer must match the current claim. Object-key order is not a change;
217
+ field values and producer presence are. Metadata edits make `status`
218
+ stale and require a fresh probe, even when the fault and defender inputs
219
+ are identical. A reused record must never be silently relabelled as a
220
+ different requirement or origin. Legacy records remain readable; missing
221
+ metadata cannot establish equality with a current declaration. This
222
+ does not change verdicts or baseline fingerprints, and a source label
223
+ still does not authenticate independent intent.
224
+
90
225
  9. **The fault must be the code that ran.** A green baseline proves the
91
226
  harness is not reporting everything as broken. It proves nothing about the
92
227
  opposite direction: a fault the interpreter never executes leaves the
@@ -446,6 +581,69 @@ Security- and money-shaped path names are explicit tie-breaking signals, never
446
581
  semantic claims about a file. Churn, path risk and claim coverage remain raw
447
582
  facts: they are never collapsed into one health score.
448
583
 
584
+ ### Intent-first scaffold authoring
585
+
586
+ Unannotated generated draft groups use unfinished `TODO-CLAIM-N` identifiers,
587
+ not file/function-shaped names. Allocation is deterministic in scan order and
588
+ reserves explicit annotations and supplied existing IDs before choosing ordinals.
589
+ It never copies unrelated existing metadata into a generated placeholder. Explicit
590
+ claim IDs/annotations retain their existing behavior; neither annotations nor
591
+ placeholder IDs authenticate intent. Grouping remains mechanical candidate
592
+ organization, not invariant ownership. Sweep's pooled allocator keeps repeated
593
+ per-file placeholders distinct without changing fault content. Existing claims
594
+ and recorded evidence are not renamed. The claims schema/validator is unchanged:
595
+ these are valid draft IDs and still require supplied intent and review/probing.
596
+
597
+ Explicit `scaffold <source...> --into <existing-draft.json> --claim <ID>` appends
598
+ mechanically derived faults only to that existing selected claim. Preview JSON
599
+ is a conforming claims document (the existing claims schema and validator), not
600
+ verification evidence. `--json` creates no files, locks or recovery. Without it,
601
+ only the explicitly selected disposable draft is updated; unchanged repeats
602
+ preserve exact bytes/mtime. Supplied statements/origins/metadata and existing
603
+ faults/sibling claims remain intact. Review and probe are still required.
604
+
605
+ `--into` is scaffold-only, singular and requires one nonempty existing claim ID
606
+ and at least one source. Repeated/comma selections, `--out`, `--claims` and any
607
+ explicit unrelated option refuse with usage exit 3 before reading/writing.
608
+ Help/version retain their normal early-exit behavior. Ordinary scaffold remains
609
+ one-file; annotation `--apply` retains its separate claims-only meaning.
610
+
611
+ Refuse canonical `testguard.claims.json` names and built-in evidence/baseline
612
+ destinations, unsafe aliases/symlinks/hardlinks/nested projects and nonregular
613
+ inputs. Admission limits are 32 sources, 2 MiB per source/draft, 16 MiB aggregate
614
+ source bytes, 4096 generated proposals and 2 MiB serialized output. Caps refuse,
615
+ never truncate proposals. These are not whole-command performance guarantees.
616
+
617
+ Changed drafts require a cooperative owned exclusive lock, current input replay,
618
+ external private original/started/result recovery journals, nofollow descriptor
619
+ publication through `writeSpecDoc`, short-write handling, fsync and exact
620
+ byte/schema/path/source verification. Refusal, stale inputs, partial writes,
621
+ verification or cleanup/journal failure exit 2, report recovery when available,
622
+ and never print updated success. No automatic rollback, stale-lock reclamation,
623
+ atomic multi-file snapshot or power-loss transaction is promised. Exit 0 means
624
+ authoring completed, not defended coverage. Private recovery is not evidence.
625
+
626
+ Sweep pools per-file drafts before selection. Repeated claim IDs from equal
627
+ basenames/functions or annotations do not merge unrelated claims: reserve all
628
+ original IDs, then allocate unused numeric suffixes to collisions in full
629
+ target-path/original-ID order, within the existing 128-character limit. Metadata,
630
+ fault IDs, injection anchors and defenders are preserved. Drafts, selected probe
631
+ claims and finding lookup share these identities. This affects fresh collision
632
+ cases only; existing documents, verdicts, caps and exit rules remain compatible.
633
+
634
+ Scaffold's mechanically generated new claims declare `source.kind: inferred`,
635
+ including proposals grouped by an annotation. Their TODO asks for intended
636
+ observable behavior from a requirement, ADR, bug or incident: inputs, expected
637
+ outcome and forbidden outcome. Existing supplied claim metadata retains its
638
+ statement and declared origin under the existing `--claim` rules. Derived fault
639
+ provenance remains derived. No automatic source promotion or canonical write.
640
+
641
+ Human CLI, status authoring actions and the installed skill hand off independent
642
+ intent authoring. Record a supplied source kind/ref; when unavailable keep
643
+ inferred. References and annotations do not authenticate intent and are never
644
+ fetched. Passing tests do not establish independent intent. State, next action,
645
+ verdict and exit-code semantics are unchanged; this is authoring guidance.
646
+
449
647
  When existing evidence is clean but more source modules are unclaimed than
450
648
  claimed, the next action is to scaffold the highest-churn unclaimed module.
451
649
  Earlier correctness states still win: invalid anchors, stale evidence and
@@ -559,6 +757,48 @@ evidence; they simply do not turn CI red.
559
757
 
560
758
  ## Exit codes
561
759
 
760
+ ### Explicit declared-origin policy result contract
761
+
762
+ Optional `originPolicy` on evidence, status and brief (and status's probe `run`)
763
+ records an explicitly selected eligible-kind set, not authenticated independence.
764
+ Legacy absence is not a pass. `probe --require-origin <kinds>` selects a
765
+ repeatable comma-separated nonempty set from the closed source vocabulary.
766
+ It requires a complete current-project run with confirm >= 3 and refuses
767
+ `--claim`, `--allow-empty`, explicit `--ref` and `--ignore-dirty` as usage errors
768
+ before loading or writing. No policy is inferred when the option is absent.
769
+ After measurement, the command reloads current claims and collects the current
770
+ native runner universe under the same command budget, then binds targets,
771
+ resolved defenders and current discovery dependencies. Missing bindings or
772
+ discovery failure yields unavailable; custom/static runner universes cannot
773
+ certify this boundary. Expired command budgets still write no partial result.
774
+ This is point-in-time admission, not an atomic filesystem snapshot. Stored
775
+ results are not freshness authority. Offline status reuses only the recorded
776
+ eligible-kind selection, recomputes current declarations/complete identities,
777
+ and marks freshness unavailable without executing native discovery. This is
778
+ an audit projection, not implicit activation of a new gate: the existing
779
+ status state, next action and exit remain unchanged. Briefs preserve the
780
+ recorded policy before baseline filtering or item caps, labeled recorded-run
781
+ only and not current freshness. No absent policy is synthesized. A zero-item
782
+ cap cannot turn unproven faults into an all-defended message.
783
+
784
+ The closed result carries `state` (`passed`, `failed`, `unavailable`), sorted
785
+ unique `eligibleKinds`, current `claims`/`faults` denominators, sorted unique
786
+ `ineligibleClaims`, unique `nonKilledFaults` (`claimId`, `faultId` only), and
787
+ sorted unique `unavailableReasons`. References and paths are excluded.
788
+ Refusal reasons require unavailable; otherwise any ineligible claim or
789
+ non-killed fault requires failed. Only a nonempty failure-free universe can
790
+ pass. Baseline debt and severity floors cannot suppress policy failures.
791
+
792
+ Evidence validation binds actual non-killed identities, and, for an available
793
+ policy, unique recorded denominators, recorded eligibility, unmixed origins and
794
+ confirmed injection. Validation cannot authenticate current metadata or disk
795
+ freshness: evaluation must separately bind the complete current fault universe
796
+ and existing input freshness. Projected status/brief bind denominators, not
797
+ hidden source declarations. Brief policy describes its recorded run only.
798
+ Completed probe JSON with unavailable policy exits 2; failed policy or ordinary
799
+ new findings exits 1; passed policy without ordinary findings exits 0.
800
+ Partial/provisional invocations cannot carry an origin policy.
801
+
562
802
  | Code | Meaning |
563
803
  |---|---|
564
804
  | `0` | No new gating findings at or above the severity floor. |
@@ -577,3 +817,79 @@ output is the existing `no-claims` status document, without run metadata;
577
817
  it never reports a clean measurement. Missing or invalid claims still fail,
578
818
  and selected claims cannot be silently skipped. Run `gate` separately to
579
819
  enforce coverage of changed files during adoption. Integrations must opt in.
820
+
821
+ ## Read-only authoring input (candidate)
822
+
823
+ `authoring-input` is metadata-only inspection, never claims or evidence. Its
824
+ closed purpose/verification/next fields explicitly state that verification was
825
+ not performed and independent intent remains to be supplied. Documents expose
826
+ only bounded project-relative file/hash/byte metadata, not text or absolute
827
+ roots. A digest or schema-valid report does not authenticate intent or freshness.
828
+
829
+ Fix inputs retain exact same-width commit/parent/path identities, untrusted
830
+ single-line subjects, selected root/nested scope and full partition accounting.
831
+ Visible rows exclude private/delegated names; their count stays in the total.
832
+ Deleted and unsupported rows cannot become supported source. Validators reject
833
+ duplicate/unsafe/private paths, inconsistent status/mode/zero-ID relations,
834
+ disposition/count mismatches, unsafe subjects and resource overruns. Empty
835
+ documents/inventories are admitted input metadata, not successful verification.
836
+ Readers cannot infer actual history completeness or current input admission
837
+ from shape alone; the runtime must use admitted local inputs without promotion.
838
+
839
+ `scaffold --from-document <local-text-path>` or `--from-fix <full-commit-ID>`
840
+ selects exactly one read-only authoring input. Both token dispatch and direct
841
+ handler entry reject repetition, mixed modes, positional sources and unrelated
842
+ explicit options (only JSON/help/version may accompany selection). Normal
843
+ help/version early exits remain unchanged. Usage refusal exits 3; input admission
844
+ or validated planning failure exits 2 with no success stdout. Success exits 0
845
+ for inspection only, never coverage. Both JSON and human modes are stdout-only.
846
+ JSON uses the validated metadata-only report; human rendering omits text,
847
+ subjects, absolute roots and historical filenames while retaining full fix
848
+ partition counts. Both name verification as not performed and require supplied
849
+ independent intent before reviewing/probing faults. One elapsed budget covers
850
+ input reads and validated serialization; scope resolution never retries with
851
+ fresh time. Existing ordinary scaffold/append/probe behavior is unchanged.
852
+
853
+ ## Resource controls and measurement
854
+
855
+ Built-in runners default to one worker. `--workers N` raises the worker ceiling
856
+ for Vitest, Jest and Playwright; `--serial` overrides it to one. Python remains
857
+ serial and disables pytest-xdist without installing or enabling any plugin.
858
+ Faults and mixed-runner partitions remain sequential. This is a worker ceiling,
859
+ not OS CPU/memory containment; test-created threads, native libraries and custom
860
+ `--runner-cmd` commands manage their own concurrency. Native discovery is bounded
861
+ and uses one worker where applicable.
862
+
863
+ Runner stdout is drained without retention; stderr retains only its last 65,536
864
+ characters. Complete JSON reports are read under the existing 16 MiB discovery
865
+ report admission limit. An oversized, unreadable or incomplete report is an error,
866
+ never an assertion kill. No confirmation count or verdict rule changes.
867
+
868
+ Cancellation terminates only registered children and their attributable process
869
+ trees, waits up to one second for close, restores live mutations and removes the
870
+ registered scratch worktrees. It exits 130 without publishing partial evidence.
871
+ Reparented daemons remain outside the portable containment guarantee. Git
872
+ subprocesses have a 30-second hard deadline; the whole-command budget remains
873
+ cooperative around synchronous setup, and bounded OS cleanup can outlive it.
874
+
875
+ Optional `run.measurements` records monotonic elapsed time, fresh runner execution
876
+ time, invocation count and remaining setup/discovery/other overhead. Each real
877
+ runner invocation is charged once, including negative controls and mixed-runner
878
+ partitions; reused records and cached baselines create no invocation charge.
879
+ `elapsedMs = runnerMs + overheadMs`. Optional `run.workers` records the built-in
880
+ worker ceiling and must be one for serial execution; custom commands omit it.
881
+ Native evidence reuse requires the same recorded worker ceiling and serial policy.
882
+ Older native evidence with no worker ceiling is remeasured; native and opaque
883
+ custom-command policies are not interchangeable.
884
+ `status.cost.measurements` copies these measurements when available. Historical
885
+ `totalMs` and cost gates retain their existing record attribution, including
886
+ reused durations and repeated shared baseline durations. Older evidence has no
887
+ measured breakdown; consumers must not infer it from record totals.
888
+
889
+ The existing Python interpreter cache is reset at probe and replay entry and
890
+ reused within that invocation. Uncached adapter resolution consumes the same
891
+ live per-run deadline as test execution.
892
+
893
+ The regression suite caps outer concurrency at two workers in CI. Interactive
894
+ runs reserve one available CPU and use at most two workers; a two-core machine
895
+ therefore uses one. Nested built-in runners retain their one-worker default.
package/spec/README.md CHANGED
@@ -1,7 +1,14 @@
1
1
  # claimspec — the contract spine
2
2
 
3
- Nine formats for declaring what must be true about a codebase, recording the
4
- evidence that tried to falsify it, and gating only what is new.
3
+ The candidate optional `originPolicy` result is a closed declared-origin policy
4
+ audit on evidence/status/brief. It is not independent-origin authentication or
5
+ current freshness authority. `probe --require-origin` explicitly evaluates it
6
+ against current native/input bindings. Offline status recomputes declarations
7
+ without certifying freshness; brief preserves labeled recorded-run policy
8
+ before filtering/caps. See GATE-SEMANTICS.md.
9
+
10
+ Shared formats for declaring what must be true about a codebase, recording the
11
+ evidence that tried to falsify it, gating only what is new, and explicit authoring.
5
12
 
6
13
  They are shared as *formats*, not as code. The first tools to adopt them are
7
14
  [`docguard-cli`](https://www.npmjs.com/package/docguard-cli) (Node ESM) and
@@ -33,6 +40,8 @@ that carry claims, baselines, scoping and briefs do not care.
33
40
  | `status` | [`schemas/status.schema.json`](schemas/status.schema.json) | Where the project is and what happens next — the single machine-readable truth every human rendering derives from. Surfaces faults whose content changed since they were probed, and changed files that carry no claim. |
34
41
  | `replay` | [`schemas/replay.schema.json`](schemas/replay.schema.json) | Would this suite have caught the bugs that already escaped? Replays real fix commits: revert the source, remove the test the fix shipped, run what remains. A replayed bug is ground truth — a human already confirmed it was a defect — which is what a fault model is calibrated against. |
35
42
  | `gate` | [`schemas/gate.schema.json`](schemas/gate.schema.json) | Claim coverage of one change: which changed files carry a claim, which are excused (and by which ignore entry), which are unclaimed. The delta gate for code that has no claim yet. |
43
+ | `annotations` | [`schemas/annotations.schema.json`](schemas/annotations.schema.json) | Explicit file-header authoring preview/apply, refusals and recoverable partial failures. Never verification, a probe verdict, or authentication of intent; internal source contents are excluded. |
44
+ | `authoring-input` | [`schemas/authoring-input.schema.json`](schemas/authoring-input.schema.json) | Candidate metadata-only document/fix input handoff. No source text, origin assignment, claims or verification; historical partitions retain excluded/deleted/unsupported counts. CLI exposure remains pending. |
36
45
 
37
46
  Shared definitions (verdicts, fault classes, provenance, annotations) live in
38
47
  [`schemas/common.schema.json`](schemas/common.schema.json). Gate behaviour —
@@ -48,6 +57,22 @@ rule is in `GATE-SEMANTICS.md`.
48
57
 
49
58
  Schemas are JSON Schema 2020-12 and identified as `urn:claimspec:v1:<kind>`.
50
59
 
60
+ Claim sources share `common#/$defs/claimSourceKind`: `spec`, `adr`, `annotation`,
61
+ `comment`, `manual`, `doc`, `bug`, `incident`, `review`, `inferred`. They declare
62
+ intent origin, not authenticated independence or an evidence-strength ranking.
63
+ References remain opaque metadata; producer provenance is separate. Existing
64
+ documents remain readable, but old closed-schema readers reject the new kinds.
65
+ Optional `origins` contracts on status, evidence and brief separate distinct
66
+ claim counts from record counts. Evidence validation recomputes every kind and
67
+ mixed declaration from all records; status/brief can validate only arithmetic
68
+ and document totals because source projections are omitted. References are
69
+ never echoed in these summaries. Legacy documents without them remain valid;
70
+ older closed readers reject the additive field. Status emits current declared
71
+ origins; probe evidence and evidence-backed briefs emit recorded origins before
72
+ filtering/caps. Human renderings label these declarations as unauthenticated.
73
+ Opt-in origin policy is available on complete confirmed candidate probes;
74
+ offline projections remain audits, never current native freshness. See `GATE-SEMANTICS.md`.
75
+
51
76
  ## Conformance
52
77
 
53
78
  `lib/validate.mjs` validates a document of any kind: JSON Schema first, then
@@ -0,0 +1,40 @@
1
+ import { extname } from 'node:path';
2
+
3
+ const PRIVATE = new Set(['.local', '.wolf', '.git', '.testguard', 'graphify-out']);
4
+ const SOURCE = new Set(['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts', '.jsx', '.tsx', '.py']);
5
+
6
+ /** Shape validation runs first. These checks cannot authenticate the actual input. */
7
+ export function authoringInputErrors(doc) {
8
+ const errors = [];
9
+ const fail = (path, message) => errors.push({ path, message });
10
+ const checkPath = (file, path) => {
11
+ if (Buffer.byteLength(file) > 4096 || file.includes('\\') || /[\x00-\x1f\x7f-\x9f]/.test(file) || /^[A-Za-z]:/.test(file) || file.split('/').some(part => !part || part === '.' || part === '..' || PRIVATE.has(part))) fail(path, 'input path must be bounded, canonical and outside private/tooling namespaces');
12
+ };
13
+ if (Buffer.byteLength(JSON.stringify(doc)) > 1024 * 1024) fail('/', 'authoring input report exceeds byte limit');
14
+ const input = doc.input;
15
+ if (input.kind === 'document') {
16
+ checkPath(input.file, '/input/file');
17
+ if (!['.md', '.txt', '.rst', '.adoc'].includes(extname(input.file))) fail('/input/file', 'document format is unsupported');
18
+ return errors;
19
+ }
20
+ if (input.commit.length !== input.parent.length || input.commit === input.parent) fail('/input/parent', 'fix requires a distinct same-width parent identity');
21
+ if (Buffer.byteLength(input.subject) > 1024 || /[\x00-\x1f\x7f-\x9f]/.test(input.subject)) fail('/input/subject', 'commit subject must be bounded untrusted single-line text');
22
+ const { total, supported, deleted, unsupported, excluded } = input.counts;
23
+ if (total !== supported + deleted + unsupported + excluded || input.paths.length !== supported + deleted + unsupported) fail('/input/counts', 'complete path partition must retain excluded/deleted/unsupported denominators');
24
+ const seen = new Set(), actual = { supported: 0, deleted: 0, unsupported: 0 };
25
+ input.paths.forEach((row, i) => {
26
+ const path = `/input/paths/${i}`;
27
+ checkPath(row.file, `${path}/file`);
28
+ if (seen.has(row.file)) fail(`${path}/file`, 'visible path is duplicated');
29
+ seen.add(row.file);
30
+ if (row.oldId.length !== input.commit.length || row.newId.length !== input.commit.length) fail(path, 'path object identities must match commit width');
31
+ const oldZero = /^0+$/.test(row.oldId), newZero = /^0+$/.test(row.newId);
32
+ if ((row.oldMode === '000000') !== oldZero || (row.newMode === '000000') !== newZero ||
33
+ (row.status === 'A' ? !oldZero || newZero : row.status === 'D' ? oldZero || !newZero : oldZero || newZero)) fail(path, 'mode, status and zero identity must agree');
34
+ const disposition = row.status === 'D' ? 'deleted' : !['100644', '100755'].includes(row.newMode) || !SOURCE.has(extname(row.file)) ? 'unsupported' : 'supported';
35
+ if (row.disposition !== disposition) fail(`${path}/disposition`, 'disposition must derive from actual status, mode and supported extension');
36
+ actual[row.disposition]++;
37
+ });
38
+ if (actual.supported !== supported || actual.deleted !== deleted || actual.unsupported !== unsupported) fail('/input/counts', 'visible disposition counts must reproduce every path');
39
+ return errors;
40
+ }
@@ -0,0 +1,56 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { isDeepStrictEqual } from 'node:util';
3
+
4
+ // One vocabulary for admission and reporting; never an evidence-strength order.
5
+ const common = JSON.parse(readFileSync(new URL('../schemas/common.schema.json', import.meta.url), 'utf8'));
6
+ export const CLAIM_SOURCE_KINDS = Object.freeze([...common.$defs.claimSourceKind.enum]);
7
+ const kindSet = new Set(CLAIM_SOURCE_KINDS);
8
+ const counts = () => Object.fromEntries(CLAIM_SOURCE_KINDS.map((kind) => [kind, 0]));
9
+
10
+ // Consumers validate their full document first. These narrow guards prevent
11
+ // malformed direct calls from silently manufacturing a meaningful summary.
12
+ function declaration(claim) {
13
+ const source = claim?.source;
14
+ if (typeof claim?.id !== 'string' || !claim.id || !kindSet.has(source?.kind)
15
+ || Object.keys(source).some((key) => key !== 'kind' && key !== 'ref')
16
+ || (Object.hasOwn(source, 'ref') && (typeof source.ref !== 'string' || source.ref.length > 512))) {
17
+ throw new TypeError('origin summary needs valid claim identities and declared sources');
18
+ }
19
+ return { kind: source.kind, ...(Object.hasOwn(source, 'ref') ? { ref: source.ref } : {}) };
20
+ }
21
+
22
+ /** Current declarations only: fault multiplicity cannot inflate claim counts. */
23
+ export function declaredOriginSummary(claims) {
24
+ const byKind = counts();
25
+ const seen = new Set();
26
+ for (const claim of claims) {
27
+ const source = declaration(claim);
28
+ if (seen.has(claim.id)) throw new TypeError('duplicate declared claim identity');
29
+ seen.add(claim.id);
30
+ byKind[source.kind]++;
31
+ }
32
+ return { basis: 'declared', claims: { total: seen.size, byKind, mixed: 0 } };
33
+ }
34
+
35
+ /** Actual recorded projections only; no current-claims relabeling or filtering. */
36
+ export function recordedOriginSummary(records) {
37
+ const recordKinds = counts();
38
+ const claims = new Map();
39
+ for (const record of records) {
40
+ const source = declaration(record.claim);
41
+ recordKinds[source.kind]++;
42
+ const previous = claims.get(record.claim.id);
43
+ if (!previous) claims.set(record.claim.id, { source, mixed: false });
44
+ else if (!isDeepStrictEqual(previous.source, source)) previous.mixed = true;
45
+ }
46
+ const byKind = counts();
47
+ let mixed = 0;
48
+ for (const claim of claims.values()) {
49
+ if (claim.mixed) mixed++;
50
+ else byKind[claim.source.kind]++;
51
+ }
52
+ return {
53
+ basis: 'recorded', claims: { total: claims.size, byKind, mixed },
54
+ records: { total: records.length, byKind: recordKinds },
55
+ };
56
+ }