@encore-os/eos-spec 0.16.0 → 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 (49) hide show
  1. package/dist/commands/close-out.js +48 -2
  2. package/dist/commands/close-out.js.map +1 -1
  3. package/dist/commands/doctor.js +14 -5
  4. package/dist/commands/doctor.js.map +1 -1
  5. package/dist/commands/index-spec.js +22 -4
  6. package/dist/commands/index-spec.js.map +1 -1
  7. package/dist/commands/next.d.ts +6 -0
  8. package/dist/commands/next.js +11 -1
  9. package/dist/commands/next.js.map +1 -1
  10. package/dist/commands/reconcile.js +6 -2
  11. package/dist/commands/reconcile.js.map +1 -1
  12. package/dist/commands/scorecard.js +51 -0
  13. package/dist/commands/scorecard.js.map +1 -1
  14. package/dist/commands/signoff.js +12 -1
  15. package/dist/commands/signoff.js.map +1 -1
  16. package/dist/commands/verify-acs.d.ts +7 -0
  17. package/dist/commands/verify-acs.js +61 -8
  18. package/dist/commands/verify-acs.js.map +1 -1
  19. package/dist/lib/closeout-core.d.ts +15 -0
  20. package/dist/lib/closeout-core.js +42 -5
  21. package/dist/lib/closeout-core.js.map +1 -1
  22. package/dist/lib/config.d.ts +24 -2
  23. package/dist/lib/config.js.map +1 -1
  24. package/dist/lib/doctor-core.d.ts +17 -2
  25. package/dist/lib/doctor-core.js +50 -12
  26. package/dist/lib/doctor-core.js.map +1 -1
  27. package/dist/lib/frontmatter.js +26 -3
  28. package/dist/lib/frontmatter.js.map +1 -1
  29. package/dist/lib/reconcile-core.d.ts +1 -1
  30. package/dist/lib/reconcile-core.js +10 -2
  31. package/dist/lib/reconcile-core.js.map +1 -1
  32. package/dist/lib/rtm-scorecard-run.d.ts +38 -0
  33. package/dist/lib/rtm-scorecard-run.js +70 -0
  34. package/dist/lib/rtm-scorecard-run.js.map +1 -0
  35. package/dist/lib/rtm-scorecard.d.ts +151 -0
  36. package/dist/lib/rtm-scorecard.js +197 -0
  37. package/dist/lib/rtm-scorecard.js.map +1 -0
  38. package/dist/lib/spec-pure.d.ts +38 -0
  39. package/dist/lib/spec-pure.js +162 -17
  40. package/dist/lib/spec-pure.js.map +1 -1
  41. package/dist/lib/specs.d.ts +20 -0
  42. package/dist/lib/specs.js +37 -1
  43. package/dist/lib/specs.js.map +1 -1
  44. package/dist/lib/version.d.ts +1 -1
  45. package/dist/lib/version.js +1 -1
  46. package/dist/pure.d.ts +1 -0
  47. package/dist/pure.js +3 -0
  48. package/dist/pure.js.map +1 -1
  49. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"reconcile-core.js","sourceRoot":"","sources":["../../src/lib/reconcile-core.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAC7F,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAuBvD;;oFAEoF;AACpF,MAAM,UAAU,QAAQ,CACtB,OAA2B,EAC3B,QAA4B,EAC5B,SAAsC,IAAI;IAE1C,IAAI,OAAO,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACtC,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC;AACvE,CAAC;AAUD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAC3B,QAAgB,EAChB,IAAc,EACd,KAAc,EACd,QAA4B;IAE5B,MAAM,OAAO,GAAG,QAAQ,EAAE,YAAY,IAAI,YAAY,CAAC;IACvD,MAAM,MAAM,GAAG,QAAQ,EAAE,IAAI,IAAI,IAAI,CAAC;IACtC,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC;IAC5C,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IACtD,IAAI,KAAK,IAAI,QAAQ,EAAE,CAAC;QACtB,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACjD,IAAI,IAAI,GAAG,QAAQ,CAAC;QACpB,IAAI,SAAS,KAAK,IAAI;YAAE,IAAI,GAAG,iBAAiB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QACjE,+EAA+E;QAC/E,6EAA6E;QAC7E,8EAA8E;QAC9E,+EAA+E;QAC/E,2EAA2E;QAC3E,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,MAAM,UAAU,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;YAC5F,MAAM,IAAI,GAAG,oBAAoB,CAAC,IAAI,EAAE,SAAS,IAAI,CAAC,IAAI,UAAU,IAAI,CAAC,EAAE,WAAW,EAAE,UAAU,CAAC,CAAC;YACpG,IAAI,IAAI;gBAAE,IAAI,GAAG,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAChD,CAAC;QACD,IAAI,IAAI,KAAK,QAAQ;YAAE,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AACxE,CAAC"}
1
+ {"version":3,"file":"reconcile-core.js","sourceRoot":"","sources":["../../src/lib/reconcile-core.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAC7F,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAuBvD;;oFAEoF;AACpF,MAAM,UAAU,QAAQ,CACtB,OAA2B,EAC3B,QAA4B,EAC5B,SAAsC,IAAI;IAE1C,IAAI,OAAO,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACtC,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC;AACvE,CAAC;AAUD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAC3B,QAAgB,EAChB,IAAc,EACd,KAAc,EACd,QAA4B,EAC5B,gBAAyB;IAEzB,MAAM,OAAO,GAAG,QAAQ,EAAE,YAAY,IAAI,YAAY,CAAC;IACvD,MAAM,MAAM,GAAG,QAAQ,EAAE,IAAI,IAAI,IAAI,CAAC;IACtC,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC;IAC5C,iEAAiE;IACjE,yFAAyF;IACzF,iFAAiF;IACjF,qFAAqF;IACrF,0FAA0F;IAC1F,sFAAsF;IACtF,2EAA2E;IAC3E,MAAM,cAAc,GAAG,gBAAgB,KAAK,SAAS,IAAI,OAAO,KAAK,gBAAgB,CAAC;IACtF,MAAM,SAAS,GAAG,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC9E,IAAI,KAAK,IAAI,QAAQ,EAAE,CAAC;QACtB,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACjD,IAAI,IAAI,GAAG,QAAQ,CAAC;QACpB,IAAI,SAAS,KAAK,IAAI;YAAE,IAAI,GAAG,iBAAiB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QACjE,+EAA+E;QAC/E,6EAA6E;QAC7E,8EAA8E;QAC9E,+EAA+E;QAC/E,2EAA2E;QAC3E,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,MAAM,UAAU,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;YAC5F,MAAM,IAAI,GAAG,oBAAoB,CAAC,IAAI,EAAE,SAAS,IAAI,CAAC,IAAI,UAAU,IAAI,CAAC,EAAE,WAAW,EAAE,UAAU,CAAC,CAAC;YACpG,IAAI,IAAI;gBAAE,IAAI,GAAG,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAChD,CAAC;QACD,IAAI,IAAI,KAAK,QAAQ;YAAE,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AACxE,CAAC"}
@@ -0,0 +1,38 @@
1
+ export type RtmSpecRow = {
2
+ id: string;
3
+ core: string;
4
+ fr: {
5
+ total: number;
6
+ verified: number;
7
+ unverified: string[];
8
+ };
9
+ sc: {
10
+ total: number;
11
+ met: number;
12
+ unmet: string[];
13
+ };
14
+ };
15
+ export type RtmReport = {
16
+ core: string;
17
+ /** Specs scanned. */
18
+ total: number;
19
+ /** Specs carrying ≥1 FR or SC (the ones the scorecard actually scores). */
20
+ specsWithReqs: number;
21
+ fr: {
22
+ total: number;
23
+ verified: number;
24
+ };
25
+ sc: {
26
+ total: number;
27
+ met: number;
28
+ };
29
+ /** Fraction of FRs verified (0–1); 1 when there are no FRs (nothing to verify). */
30
+ frVerifiedPct: number;
31
+ /** Fraction of SCs met (0–1); 1 when there are no SCs. */
32
+ scMetPct: number;
33
+ /** Per-spec rows, only for specs that carry FR/SC requirements. */
34
+ rows: RtmSpecRow[];
35
+ };
36
+ /** Build the corpus RTM scorecard for `coreFilter` (or all cores). Deterministic (listSpecs
37
+ * order). Specs with no FR/SC requirements are counted in `total` but omitted from `rows`. */
38
+ export declare function buildRtmScorecard(repoRoot: string, coreFilter?: string): RtmReport;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * fs-bound driver for the FR/SC RTM scorecard (#79): walk a repo's specs, read each spec's
3
+ * body + frontmatter ACs, and aggregate the pure {@link deriveScorecard} per spec into a corpus
4
+ * report. The verdict logic + parsing stay in the pure `rtm-scorecard.ts` (on the `/pure` graph);
5
+ * this only supplies the corpus walk + file IO, so it lives behind the package main entry.
6
+ *
7
+ * On-disk (proof-free) by design: "verified" reads from each AC's frontmatter `status` and each
8
+ * FR's body `[status]`, not from a live test run. A consumer that wants run-proven coverage feeds
9
+ * `deriveScorecard` its own `proof` set from `verify-acs`; this static readiness view mirrors the
10
+ * existing `scorecard` command.
11
+ */
12
+ import { readFileSync } from 'node:fs';
13
+ import { readFrontmatter, stripFrontmatter } from './frontmatter.js';
14
+ import { collectScorecardItems, deriveScorecard } from './rtm-scorecard.js';
15
+ import { listSpecs } from './specs.js';
16
+ /** Build the corpus RTM scorecard for `coreFilter` (or all cores). Deterministic (listSpecs
17
+ * order). Specs with no FR/SC requirements are counted in `total` but omitted from `rows`. */
18
+ export function buildRtmScorecard(repoRoot, coreFilter) {
19
+ const specs = listSpecs(repoRoot, coreFilter);
20
+ const rows = [];
21
+ let frTotal = 0;
22
+ let frVerified = 0;
23
+ let scTotal = 0;
24
+ let scMet = 0;
25
+ let specsWithReqs = 0;
26
+ for (const spec of specs) {
27
+ let text = '';
28
+ try {
29
+ text = readFileSync(spec.path, 'utf8');
30
+ }
31
+ catch {
32
+ continue; // unreadable — skip
33
+ }
34
+ const fm = readFrontmatter(text);
35
+ const items = collectScorecardItems(stripFrontmatter(text), fm.acceptance_criteria);
36
+ const card = deriveScorecard(items);
37
+ if (card.fr.total === 0 && card.sc.total === 0)
38
+ continue; // nothing to score
39
+ specsWithReqs++;
40
+ frTotal += card.fr.total;
41
+ frVerified += card.fr.verified;
42
+ scTotal += card.sc.total;
43
+ scMet += card.sc.met;
44
+ rows.push({
45
+ id: spec.id,
46
+ core: spec.core,
47
+ fr: {
48
+ total: card.fr.total,
49
+ verified: card.fr.verified,
50
+ unverified: card.fr.rows.filter((r) => !r.verified).map((r) => r.id),
51
+ },
52
+ sc: {
53
+ total: card.sc.total,
54
+ met: card.sc.met,
55
+ unmet: card.sc.rows.filter((r) => !r.met).map((r) => r.id),
56
+ },
57
+ });
58
+ }
59
+ return {
60
+ core: coreFilter ?? 'all',
61
+ total: specs.length,
62
+ specsWithReqs,
63
+ fr: { total: frTotal, verified: frVerified },
64
+ sc: { total: scTotal, met: scMet },
65
+ frVerifiedPct: frTotal === 0 ? 1 : frVerified / frTotal,
66
+ scMetPct: scTotal === 0 ? 1 : scMet / scTotal,
67
+ rows,
68
+ };
69
+ }
70
+ //# sourceMappingURL=rtm-scorecard-run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rtm-scorecard-run.js","sourceRoot":"","sources":["../../src/lib/rtm-scorecard-run.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The FR/SC Requirements-Traceability-Matrix (RTM) scorecard — a NON-VACUOUS coverage
3
+ * derivation folded into the engine from encoreos's repo-local `scripts/specs/verify-acs.mjs`
4
+ * (issue #79). It answers "how much of this spec's Definition of Done (its functional
5
+ * requirements FR-N and success criteria SC-N) is actually proven?" — where "proven" is
6
+ * defined so a coverage-free corpus cannot read 100% (the vacuity guard):
7
+ *
8
+ * - an **FR** is verified iff it has ≥1 covering AC and every covering AC is verified,
9
+ * OR it carries its own proof. An FR with zero covering ACs and no own-proof is NOT
10
+ * verified — that is the property the vacuity guard pins.
11
+ * - an **SC** is met iff it references ≥1 AC, at least one referenced AC is verified,
12
+ * AND every NFR it gates is green. An SC that references no AC is NOT met.
13
+ *
14
+ * PURE (fs-free): it reads a spec's body text + already-parsed `acceptance_criteria`, so it
15
+ * sits on the `/pure` graph and a browser bundle (Spec Studio) can compute the scorecard
16
+ * client-side. The corpus walk + file IO that feed it stay in the command layer. Trace edges
17
+ * (AC→FR via `fr_refs`, SC→AC/NFR via reference parens) mirror the engine's existing model.
18
+ */
19
+ /** An AC block the scorecard reasons over: its id, terminal status, and the FR/NFR ids it
20
+ * covers (from frontmatter `fr_refs` / `nfr_refs`). Sourced from the parsed frontmatter. */
21
+ export type ScorecardAcBlock = {
22
+ id: string;
23
+ status?: string;
24
+ fr_refs?: string[];
25
+ nfr_refs?: string[];
26
+ };
27
+ /** A body requirement/NFR item (`- **FR-1** [status] …`). */
28
+ export type ScorecardRequirementItem = {
29
+ id: string;
30
+ status: string;
31
+ };
32
+ /** A success criterion parsed from the body, with the ACs/NFRs it traces to. */
33
+ export type SuccessCriterionItem = {
34
+ id: string;
35
+ status: string;
36
+ ac_refs: string[];
37
+ nfr_refs: string[];
38
+ };
39
+ /** Proof accumulated from a verification run: AC/FR ids proven by a passing backing test.
40
+ * Empty ⇒ the scorecard reflects on-disk statuses only (still non-vacuous). */
41
+ export type ScorecardProof = {
42
+ provenAcIds?: Set<string>;
43
+ provenFrIds?: Set<string>;
44
+ };
45
+ export type FrVerdict = {
46
+ verified: boolean;
47
+ reason: string;
48
+ coveringAcs: string[];
49
+ };
50
+ export type ScVerdictResult = {
51
+ met: boolean;
52
+ reason: string;
53
+ };
54
+ export type Scorecard = {
55
+ fr: {
56
+ total: number;
57
+ verified: number;
58
+ rows: Array<{
59
+ id: string;
60
+ } & FrVerdict>;
61
+ };
62
+ sc: {
63
+ total: number;
64
+ met: number;
65
+ rows: Array<{
66
+ id: string;
67
+ } & ScVerdictResult>;
68
+ };
69
+ };
70
+ /** Parse a single body line into `{ id, kind: 'US' | 'FR', status }`, or null. US and FR
71
+ * share one list format (`- **FR-1** [verified] …`); FR is first-class verifiable. */
72
+ export declare function matchBodyRequirement(line: string): {
73
+ id: string;
74
+ kind: 'US' | 'FR';
75
+ status: string;
76
+ } | null;
77
+ /** Parse one body line into a success criterion `{ id, status, ac_refs, nfr_refs }`, or null.
78
+ * SC lines carry trailing reference parens — `(US-3, AC-6)`, `(US-1, FR-1, NFR-1)` — from
79
+ * which the AC and NFR ids are scraped (dedup'd). */
80
+ export declare function matchSuccessCriterion(line: string): SuccessCriterionItem | null;
81
+ /** Match a body NFR item (`- **NFR-perf-1** [status] …`) — the gates an SC verdict checks. */
82
+ export declare function matchNfrItem(line: string): ScorecardRequirementItem | null;
83
+ /** Invert the AC→FR trace edges (each AC's `fr_refs`) into `FR id → [covering AC ids]`. */
84
+ export declare function coveringAcsByFr(acBlocks: ScorecardAcBlock[]): Map<string, string[]>;
85
+ /** True if an AC is verified — status `verified` on disk OR proven in this run. */
86
+ export declare function isAcVerified(acId: string, acStatusById: Map<string, string | undefined>, provenAcIds: Set<string>): boolean;
87
+ /**
88
+ * The NON-VACUOUS FR verdict. An FR counts verified iff (a) it has ≥1 covering AC and every
89
+ * covering AC is verified, or (b) the FR itself is proven. Zero covering ACs and no own-proof
90
+ * ⇒ NOT verified (the vacuity guard: a coverage-free corpus cannot read 100%).
91
+ */
92
+ export declare function frVerdict(opts: {
93
+ coveringAcs?: string[];
94
+ acStatusById: Map<string, string | undefined>;
95
+ provenAcIds: Set<string>;
96
+ frProven?: boolean;
97
+ frVerifiedOnDisk?: boolean;
98
+ }): FrVerdict;
99
+ /**
100
+ * The NON-VACUOUS SC verdict. An SC counts met iff (a) it references ≥1 AC and at least one
101
+ * referenced AC is verified, and (b) every NFR it gates is green. A missing NFR (not in the
102
+ * spec) is ungreen — a dangling gate cannot be assumed satisfied. An SC referencing no AC is
103
+ * NOT met.
104
+ */
105
+ export declare function scVerdict(sc: SuccessCriterionItem, opts: {
106
+ acStatusById: Map<string, string | undefined>;
107
+ provenAcIds: Set<string>;
108
+ nfrVerifiedById: Map<string, boolean>;
109
+ }): ScVerdictResult;
110
+ /** The items one spec's scorecard reasons over (body-parsed FR/SC/NFR + frontmatter AC blocks). */
111
+ export type ScorecardItems = {
112
+ frItems: ScorecardRequirementItem[];
113
+ acBlocks: ScorecardAcBlock[];
114
+ scItems: SuccessCriterionItem[];
115
+ nfrItems: ScorecardRequirementItem[];
116
+ };
117
+ /**
118
+ * Collect the scorecard items for one spec from its BODY text (FR/US/NFR/SC list lines) and
119
+ * its already-parsed `acceptance_criteria` frontmatter (AC id/status/fr_refs/nfr_refs). Pure —
120
+ * the caller supplies the body (frontmatter-stripped) and the raw AC array. US items are not
121
+ * scored (they trace via ACs) so they are not returned.
122
+ */
123
+ export declare function collectScorecardItems(bodyText: string, rawAcs: unknown): ScorecardItems;
124
+ /** Derive the whole FR/SC scorecard for one spec — the single non-vacuous entry point. */
125
+ export declare function deriveScorecard(spec: Partial<ScorecardItems>, proof?: ScorecardProof): Scorecard;
126
+ /** Advisory coverage ratchet floors (warn-level, NEVER fatal): the fraction of FRs that clear
127
+ * the non-vacuous bar and of SCs that are met. Conservative defaults; a consumer overrides via
128
+ * `rtm.ratchets` in `specs/config.yaml`. Kept advisory so a young/low-coverage corpus stays
129
+ * green while a real regression below the floor still surfaces. */
130
+ export declare const COVERAGE_RATCHET_FLOORS: Record<string, number>;
131
+ /** The resolved RTM config: whether the scorecard is enabled + the effective ratchet floors.
132
+ * `resolveRtm` duck-types the config (no fs-bound `SpecConfig` import) so this stays pure. */
133
+ export type RtmConfig = {
134
+ enabled: boolean;
135
+ ratchets: Record<string, number>;
136
+ };
137
+ /** Resolve the opt-in RTM scorecard config; defaults to disabled + the built-in floors, so a
138
+ * repo without an `rtm:` block (or with `enabled` falsy) gets no scorecard. */
139
+ export declare function resolveRtm(config: {
140
+ rtm?: {
141
+ enabled?: boolean;
142
+ ratchets?: Record<string, number>;
143
+ };
144
+ } | null | undefined): RtmConfig;
145
+ /** Compare measured coverage (`{ frVerifiedPct, scMetPct, … }`, each 0–1) against the advisory
146
+ * floors. Returns the breaches (empty ⇒ none). Pure — the caller decides whether to print/warn. */
147
+ export declare function checkCoverageRatchets(measured: Record<string, number>, floors?: Record<string, number>): Array<{
148
+ metric: string;
149
+ floor: number;
150
+ value: number;
151
+ }>;
@@ -0,0 +1,197 @@
1
+ /**
2
+ * The FR/SC Requirements-Traceability-Matrix (RTM) scorecard — a NON-VACUOUS coverage
3
+ * derivation folded into the engine from encoreos's repo-local `scripts/specs/verify-acs.mjs`
4
+ * (issue #79). It answers "how much of this spec's Definition of Done (its functional
5
+ * requirements FR-N and success criteria SC-N) is actually proven?" — where "proven" is
6
+ * defined so a coverage-free corpus cannot read 100% (the vacuity guard):
7
+ *
8
+ * - an **FR** is verified iff it has ≥1 covering AC and every covering AC is verified,
9
+ * OR it carries its own proof. An FR with zero covering ACs and no own-proof is NOT
10
+ * verified — that is the property the vacuity guard pins.
11
+ * - an **SC** is met iff it references ≥1 AC, at least one referenced AC is verified,
12
+ * AND every NFR it gates is green. An SC that references no AC is NOT met.
13
+ *
14
+ * PURE (fs-free): it reads a spec's body text + already-parsed `acceptance_criteria`, so it
15
+ * sits on the `/pure` graph and a browser bundle (Spec Studio) can compute the scorecard
16
+ * client-side. The corpus walk + file IO that feed it stay in the command layer. Trace edges
17
+ * (AC→FR via `fr_refs`, SC→AC/NFR via reference parens) mirror the engine's existing model.
18
+ */
19
+ /** Parse a single body line into `{ id, kind: 'US' | 'FR', status }`, or null. US and FR
20
+ * share one list format (`- **FR-1** [verified] …`); FR is first-class verifiable. */
21
+ export function matchBodyRequirement(line) {
22
+ const m = /^\s*-\s+\*{0,2}((US|FR)-\d+)\*{0,2}\s+\[([a-z_]+)\]/.exec(line);
23
+ return m ? { id: m[1], kind: m[2], status: m[3] } : null;
24
+ }
25
+ /** Parse one body line into a success criterion `{ id, status, ac_refs, nfr_refs }`, or null.
26
+ * SC lines carry trailing reference parens — `(US-3, AC-6)`, `(US-1, FR-1, NFR-1)` — from
27
+ * which the AC and NFR ids are scraped (dedup'd). */
28
+ export function matchSuccessCriterion(line) {
29
+ const m = /^\s*-\s+\*\*(SC-\d+(?:\.\d+)*)\b[^*\n]*\*\*\s*(?:\[([a-z_]+)\])?\s*(.*)$/.exec(line);
30
+ if (!m)
31
+ return null;
32
+ const tail = m[3] ?? '';
33
+ return {
34
+ id: m[1],
35
+ status: m[2] ?? 'planned',
36
+ ac_refs: [...new Set(tail.match(/\bAC-\d+(?:\.\d+)*/g) ?? [])],
37
+ nfr_refs: [...new Set(tail.match(/\bNFR(?:-[a-z]+)?-\d+(?:\.\d+)*/g) ?? [])],
38
+ };
39
+ }
40
+ /** Match a body NFR item (`- **NFR-perf-1** [status] …`) — the gates an SC verdict checks. */
41
+ export function matchNfrItem(line) {
42
+ const m = /^\s*-\s+\*{0,2}(NFR(?:-[a-z]+)?-\d+(?:\.\d+)*)\*{0,2}(?:\s+\[([a-z_]+)\])?/.exec(line);
43
+ return m ? { id: m[1], status: m[2] ?? 'planned' } : null;
44
+ }
45
+ /** Invert the AC→FR trace edges (each AC's `fr_refs`) into `FR id → [covering AC ids]`. */
46
+ export function coveringAcsByFr(acBlocks) {
47
+ const map = new Map();
48
+ for (const ac of acBlocks) {
49
+ for (const fr of ac.fr_refs ?? []) {
50
+ if (!map.has(fr))
51
+ map.set(fr, []);
52
+ map.get(fr).push(ac.id);
53
+ }
54
+ }
55
+ return map;
56
+ }
57
+ /** True if an AC is verified — status `verified` on disk OR proven in this run. */
58
+ export function isAcVerified(acId, acStatusById, provenAcIds) {
59
+ return acStatusById.get(acId) === 'verified' || provenAcIds.has(acId);
60
+ }
61
+ /**
62
+ * The NON-VACUOUS FR verdict. An FR counts verified iff (a) it has ≥1 covering AC and every
63
+ * covering AC is verified, or (b) the FR itself is proven. Zero covering ACs and no own-proof
64
+ * ⇒ NOT verified (the vacuity guard: a coverage-free corpus cannot read 100%).
65
+ */
66
+ export function frVerdict(opts) {
67
+ const { coveringAcs = [], acStatusById, provenAcIds, frProven = false, frVerifiedOnDisk = false } = opts;
68
+ if (frProven || frVerifiedOnDisk) {
69
+ return { verified: true, reason: 'own @verifies proof', coveringAcs };
70
+ }
71
+ if (coveringAcs.length === 0) {
72
+ return { verified: false, reason: 'no covering AC and no own proof', coveringAcs };
73
+ }
74
+ const unverified = coveringAcs.filter((ac) => !isAcVerified(ac, acStatusById, provenAcIds));
75
+ if (unverified.length === 0) {
76
+ return { verified: true, reason: `${coveringAcs.length} covering AC(s) all verified`, coveringAcs };
77
+ }
78
+ return { verified: false, reason: `covering AC(s) not yet verified: ${unverified.join(', ')}`, coveringAcs };
79
+ }
80
+ /**
81
+ * The NON-VACUOUS SC verdict. An SC counts met iff (a) it references ≥1 AC and at least one
82
+ * referenced AC is verified, and (b) every NFR it gates is green. A missing NFR (not in the
83
+ * spec) is ungreen — a dangling gate cannot be assumed satisfied. An SC referencing no AC is
84
+ * NOT met.
85
+ */
86
+ export function scVerdict(sc, opts) {
87
+ const { acStatusById, provenAcIds, nfrVerifiedById } = opts;
88
+ const acRefs = sc.ac_refs ?? [];
89
+ const nfrRefs = sc.nfr_refs ?? [];
90
+ if (acRefs.length === 0) {
91
+ return { met: false, reason: 'references no AC (cannot be proven)' };
92
+ }
93
+ const verifiedAcs = acRefs.filter((ac) => isAcVerified(ac, acStatusById, provenAcIds));
94
+ if (verifiedAcs.length === 0) {
95
+ return { met: false, reason: `no referenced AC is verified (${acRefs.join(', ')})` };
96
+ }
97
+ const ungreenNfrs = nfrRefs.filter((nfr) => nfrVerifiedById.get(nfr) !== true);
98
+ if (ungreenNfrs.length > 0) {
99
+ return { met: false, reason: `NFR gate(s) not green: ${ungreenNfrs.join(', ')}` };
100
+ }
101
+ return { met: true, reason: `${verifiedAcs.length}/${acRefs.length} referenced AC(s) verified, NFR gates green` };
102
+ }
103
+ /**
104
+ * Collect the scorecard items for one spec from its BODY text (FR/US/NFR/SC list lines) and
105
+ * its already-parsed `acceptance_criteria` frontmatter (AC id/status/fr_refs/nfr_refs). Pure —
106
+ * the caller supplies the body (frontmatter-stripped) and the raw AC array. US items are not
107
+ * scored (they trace via ACs) so they are not returned.
108
+ */
109
+ export function collectScorecardItems(bodyText, rawAcs) {
110
+ const acBlocks = Array.isArray(rawAcs)
111
+ ? rawAcs
112
+ .filter((e) => !!e && typeof e === 'object')
113
+ .map((e) => ({
114
+ id: typeof e.id === 'string' ? e.id : '',
115
+ status: typeof e.status === 'string' ? e.status : undefined,
116
+ fr_refs: coerceIdList(e.fr_refs),
117
+ nfr_refs: coerceIdList(e.nfr_refs),
118
+ }))
119
+ .filter((a) => a.id !== '')
120
+ : [];
121
+ const frItems = [];
122
+ const nfrItems = [];
123
+ const scItems = [];
124
+ for (const line of bodyText.split('\n')) {
125
+ const req = matchBodyRequirement(line);
126
+ if (req) {
127
+ if (req.kind === 'FR')
128
+ frItems.push({ id: req.id, status: req.status });
129
+ continue; // US items are traced via ACs, not scored directly
130
+ }
131
+ const nfr = matchNfrItem(line);
132
+ if (nfr) {
133
+ nfrItems.push(nfr);
134
+ continue;
135
+ }
136
+ const sc = matchSuccessCriterion(line);
137
+ if (sc)
138
+ scItems.push(sc);
139
+ }
140
+ return { frItems, acBlocks, scItems, nfrItems };
141
+ }
142
+ /** Coerce a frontmatter ref field (`fr_refs`/`nfr_refs`) to a string id list — accepts a YAML
143
+ * array of strings or a single string; anything else ⇒ []. */
144
+ function coerceIdList(v) {
145
+ if (typeof v === 'string')
146
+ return v.trim() === '' ? [] : [v.trim()];
147
+ if (Array.isArray(v))
148
+ return v.filter((x) => typeof x === 'string' && x.trim() !== '').map((x) => x.trim());
149
+ return [];
150
+ }
151
+ /** Derive the whole FR/SC scorecard for one spec — the single non-vacuous entry point. */
152
+ export function deriveScorecard(spec, proof = {}) {
153
+ const { frItems = [], acBlocks = [], scItems = [], nfrItems = [] } = spec;
154
+ const provenAcIds = proof.provenAcIds ?? new Set();
155
+ const provenFrIds = proof.provenFrIds ?? new Set();
156
+ const acStatusById = new Map(acBlocks.map((a) => [a.id, a.status]));
157
+ const nfrVerifiedById = new Map(nfrItems.map((n) => [n.id, n.status === 'verified']));
158
+ const coveringByFr = coveringAcsByFr(acBlocks);
159
+ const frRows = frItems.map((fr) => ({
160
+ id: fr.id,
161
+ ...frVerdict({
162
+ coveringAcs: coveringByFr.get(fr.id) ?? [],
163
+ acStatusById,
164
+ provenAcIds,
165
+ frProven: provenFrIds.has(fr.id),
166
+ frVerifiedOnDisk: fr.status === 'verified',
167
+ }),
168
+ }));
169
+ const scRows = scItems.map((sc) => ({ id: sc.id, ...scVerdict(sc, { acStatusById, provenAcIds, nfrVerifiedById }) }));
170
+ return {
171
+ fr: { total: frRows.length, verified: frRows.filter((r) => r.verified).length, rows: frRows },
172
+ sc: { total: scRows.length, met: scRows.filter((r) => r.met).length, rows: scRows },
173
+ };
174
+ }
175
+ /** Advisory coverage ratchet floors (warn-level, NEVER fatal): the fraction of FRs that clear
176
+ * the non-vacuous bar and of SCs that are met. Conservative defaults; a consumer overrides via
177
+ * `rtm.ratchets` in `specs/config.yaml`. Kept advisory so a young/low-coverage corpus stays
178
+ * green while a real regression below the floor still surfaces. */
179
+ export const COVERAGE_RATCHET_FLOORS = { frVerifiedPct: 0, scMetPct: 0 };
180
+ /** Resolve the opt-in RTM scorecard config; defaults to disabled + the built-in floors, so a
181
+ * repo without an `rtm:` block (or with `enabled` falsy) gets no scorecard. */
182
+ export function resolveRtm(config) {
183
+ const rtm = config?.rtm;
184
+ return { enabled: rtm?.enabled === true, ratchets: rtm?.ratchets ?? COVERAGE_RATCHET_FLOORS };
185
+ }
186
+ /** Compare measured coverage (`{ frVerifiedPct, scMetPct, … }`, each 0–1) against the advisory
187
+ * floors. Returns the breaches (empty ⇒ none). Pure — the caller decides whether to print/warn. */
188
+ export function checkCoverageRatchets(measured, floors = COVERAGE_RATCHET_FLOORS) {
189
+ const breaches = [];
190
+ for (const [metric, floor] of Object.entries(floors)) {
191
+ const value = measured[metric];
192
+ if (typeof value === 'number' && value < floor)
193
+ breaches.push({ metric, floor, value });
194
+ }
195
+ return breaches;
196
+ }
197
+ //# sourceMappingURL=rtm-scorecard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rtm-scorecard.js","sourceRoot":"","sources":["../../src/lib/rtm-scorecard.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,22 @@ export declare const SPEC_ID_GRAMMAR: {
8
8
  readonly pattern: "<core>(-[A-Z]{1,4})?-<n>(.<n>)?(-(EN-<n>|E<n>|P<n>(.<n>)?(-EN-<n>)?))?";
9
9
  readonly caseInsensitive: true;
10
10
  };
11
+ /**
12
+ * Remove regions that legitimately QUOTE compliance-sounding words as examples,
13
+ * so the keyword heuristic in {@link detectSensitive} reads only load-bearing
14
+ * prose. Strips, per line: fenced code blocks (``` … ``` / ~~~ … ~~~), a
15
+ * `## Examples` heading section — an EXACT `Examples`/`Example` heading title only
16
+ * (so `## Examples of Regulated Data` is a normal heading, its body left as
17
+ * scannable prose) — down to the next same-or-higher heading, and Gherkin step
18
+ * lines. `Scenario:` / `Example(s):` / `Given` / `When` / `Then` openers strip
19
+ * unconditionally; `And` / `But` strip ONLY when the immediately preceding
20
+ * non-blank line (no intervening blank line) was itself a Gherkin step/opener —
21
+ * a standalone `And`/`But` prose line (e.g. a paragraph continuation) is left in
22
+ * place. Pure string transform — no fs, stays on the `/pure` graph. Callers apply
23
+ * it to the keyword-match INPUT only; opt-in/opt-out markers are still read from
24
+ * the raw text.
25
+ */
26
+ export declare function stripExampleRegions(text: string): string;
11
27
  /** True when a generated test source has ≥1 ACTIVE `test(` block (not `test.fixme`/
12
28
  * `test.describe`) and is not an AC-less placeholder. Pure (operates on source text). */
13
29
  export declare function hasActiveGeneratedTestSource(source: string): boolean;
@@ -88,6 +104,28 @@ export declare function declaredSpecId(declaredId: string | null | undefined, co
88
104
  * Returns the resolved spec id, or null when the file is not a primary spec.
89
105
  */
90
106
  export declare function resolveSpecFileId(core: string, filename: string, declaredId?: string | null): string | null;
107
+ /**
108
+ * True when an on-disk filename PARSES to a well-formed primary-spec id yet the corpus
109
+ * loader would silently DROP it ({@link resolveSpecFileId} → null) for a reason a
110
+ * frontmatter `id:` could rescue — a reserved lifecycle word (REVIEW / ANALYSIS /
111
+ * CLARIFICATION …) landed inside a real feature slug and no `id:` disambiguates it. This
112
+ * is the silent-authoring-drop class issue #76 surfaces: the file reads as spec X to a
113
+ * human but never reaches spec-index.json / navigator / reconcile / recall.
114
+ *
115
+ * Deliberately EXCLUDES bona-fide sibling artifacts that legitimately carry a parent id
116
+ * and are SUPPOSED to be non-primary, so the surfacer stays quiet on a healthy corpus:
117
+ * - dedicated CONTEXT/CLARIFICATION/ENHANCEMENTS/EXPANSION files ({@link isDedicatedArtifactFile}),
118
+ * - `{ID}-PHASE…` lifecycle files ({@link isPhaseLifecycleFile}),
119
+ * - descriptor-suffix artifacts (`-TASKS`/`-PLAN`/`-RESEARCH`/… — {@link DERIVATIVE_DESCRIPTOR_RE}),
120
+ * - and a LONE terminal descriptor (`{ID}-REVIEW`, `{ID}-ANALYSIS`): only files with real
121
+ * slug content AFTER the id (≥2 slug words) are flagged, i.e. the reserved word is
122
+ * embedded in a feature name (`{ID}-compliance-review-workflow`), not the whole slug.
123
+ *
124
+ * The residue is exactly the case the mint-time guard (spec-id-guard → {@link resolveSpecFileId})
125
+ * blocks on `create`/`promote`, made visible for files already on disk. Pure (fs-free); the
126
+ * fs walk that feeds it lives in specs.ts `findSkippedSpecFiles`.
127
+ */
128
+ export declare function isSilentlySkippedSpecFile(core: string, filename: string, declaredId?: string | null): boolean;
91
129
  /** The skip rules `collectSpecsFromDirectory` applies to one filename. */
92
130
  export declare function isPrimarySpecFile(filename: string): boolean;
93
131
  /**