@encore-os/eos-spec 0.3.1

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 (256) hide show
  1. package/LICENSE +17 -0
  2. package/bin/check-dist-fresh.js +51 -0
  3. package/bin/eos-spec.js +7 -0
  4. package/dist/commands/ac-backfill.d.ts +31 -0
  5. package/dist/commands/ac-backfill.js +198 -0
  6. package/dist/commands/ac-backfill.js.map +1 -0
  7. package/dist/commands/archive.d.ts +2 -0
  8. package/dist/commands/archive.js +101 -0
  9. package/dist/commands/archive.js.map +1 -0
  10. package/dist/commands/audit.d.ts +30 -0
  11. package/dist/commands/audit.js +107 -0
  12. package/dist/commands/audit.js.map +1 -0
  13. package/dist/commands/backfill.d.ts +41 -0
  14. package/dist/commands/backfill.js +239 -0
  15. package/dist/commands/backfill.js.map +1 -0
  16. package/dist/commands/checkArtifacts.d.ts +2 -0
  17. package/dist/commands/checkArtifacts.js +48 -0
  18. package/dist/commands/checkArtifacts.js.map +1 -0
  19. package/dist/commands/checkPlacement.d.ts +2 -0
  20. package/dist/commands/checkPlacement.js +31 -0
  21. package/dist/commands/checkPlacement.js.map +1 -0
  22. package/dist/commands/claim.d.ts +4 -0
  23. package/dist/commands/claim.js +46 -0
  24. package/dist/commands/claim.js.map +1 -0
  25. package/dist/commands/complete.d.ts +2 -0
  26. package/dist/commands/complete.js +138 -0
  27. package/dist/commands/complete.js.map +1 -0
  28. package/dist/commands/conformance.d.ts +23 -0
  29. package/dist/commands/conformance.js +124 -0
  30. package/dist/commands/conformance.js.map +1 -0
  31. package/dist/commands/create.d.ts +6 -0
  32. package/dist/commands/create.js +277 -0
  33. package/dist/commands/create.js.map +1 -0
  34. package/dist/commands/deferred.d.ts +2 -0
  35. package/dist/commands/deferred.js +85 -0
  36. package/dist/commands/deferred.js.map +1 -0
  37. package/dist/commands/delta.d.ts +17 -0
  38. package/dist/commands/delta.js +292 -0
  39. package/dist/commands/delta.js.map +1 -0
  40. package/dist/commands/freshness.d.ts +13 -0
  41. package/dist/commands/freshness.js +72 -0
  42. package/dist/commands/freshness.js.map +1 -0
  43. package/dist/commands/funnel.d.ts +2 -0
  44. package/dist/commands/funnel.js +61 -0
  45. package/dist/commands/funnel.js.map +1 -0
  46. package/dist/commands/gen-tests.d.ts +2 -0
  47. package/dist/commands/gen-tests.js +53 -0
  48. package/dist/commands/gen-tests.js.map +1 -0
  49. package/dist/commands/init.d.ts +2 -0
  50. package/dist/commands/init.js +64 -0
  51. package/dist/commands/init.js.map +1 -0
  52. package/dist/commands/list.d.ts +9 -0
  53. package/dist/commands/list.js +61 -0
  54. package/dist/commands/list.js.map +1 -0
  55. package/dist/commands/metrics.d.ts +2 -0
  56. package/dist/commands/metrics.js +66 -0
  57. package/dist/commands/metrics.js.map +1 -0
  58. package/dist/commands/next.d.ts +48 -0
  59. package/dist/commands/next.js +344 -0
  60. package/dist/commands/next.js.map +1 -0
  61. package/dist/commands/precedent.d.ts +66 -0
  62. package/dist/commands/precedent.js +343 -0
  63. package/dist/commands/precedent.js.map +1 -0
  64. package/dist/commands/ready.d.ts +28 -0
  65. package/dist/commands/ready.js +147 -0
  66. package/dist/commands/ready.js.map +1 -0
  67. package/dist/commands/reconcile.d.ts +44 -0
  68. package/dist/commands/reconcile.js +187 -0
  69. package/dist/commands/reconcile.js.map +1 -0
  70. package/dist/commands/rollup.d.ts +2 -0
  71. package/dist/commands/rollup.js +43 -0
  72. package/dist/commands/rollup.js.map +1 -0
  73. package/dist/commands/scorecard.d.ts +11 -0
  74. package/dist/commands/scorecard.js +55 -0
  75. package/dist/commands/scorecard.js.map +1 -0
  76. package/dist/commands/signoff.d.ts +2 -0
  77. package/dist/commands/signoff.js +114 -0
  78. package/dist/commands/signoff.js.map +1 -0
  79. package/dist/commands/status.d.ts +2 -0
  80. package/dist/commands/status.js +100 -0
  81. package/dist/commands/status.js.map +1 -0
  82. package/dist/commands/tasks.d.ts +13 -0
  83. package/dist/commands/tasks.js +81 -0
  84. package/dist/commands/tasks.js.map +1 -0
  85. package/dist/commands/validate.d.ts +16 -0
  86. package/dist/commands/validate.js +89 -0
  87. package/dist/commands/validate.js.map +1 -0
  88. package/dist/commands/verify-acs.d.ts +15 -0
  89. package/dist/commands/verify-acs.js +196 -0
  90. package/dist/commands/verify-acs.js.map +1 -0
  91. package/dist/commands/verifyRollout.d.ts +2 -0
  92. package/dist/commands/verifyRollout.js +125 -0
  93. package/dist/commands/verifyRollout.js.map +1 -0
  94. package/dist/index.d.ts +21 -0
  95. package/dist/index.js +138 -0
  96. package/dist/index.js.map +1 -0
  97. package/dist/lib/ac-draft-prompt.d.ts +18 -0
  98. package/dist/lib/ac-draft-prompt.js +38 -0
  99. package/dist/lib/ac-draft-prompt.js.map +1 -0
  100. package/dist/lib/ac-response-parser.d.ts +18 -0
  101. package/dist/lib/ac-response-parser.js +57 -0
  102. package/dist/lib/ac-response-parser.js.map +1 -0
  103. package/dist/lib/ai-call.d.ts +13 -0
  104. package/dist/lib/ai-call.js +17 -0
  105. package/dist/lib/ai-call.js.map +1 -0
  106. package/dist/lib/archive.d.ts +21 -0
  107. package/dist/lib/archive.js +136 -0
  108. package/dist/lib/archive.js.map +1 -0
  109. package/dist/lib/artifact-integrity.d.ts +5 -0
  110. package/dist/lib/artifact-integrity.js +65 -0
  111. package/dist/lib/artifact-integrity.js.map +1 -0
  112. package/dist/lib/backfill-queue.d.ts +11 -0
  113. package/dist/lib/backfill-queue.js +14 -0
  114. package/dist/lib/backfill-queue.js.map +1 -0
  115. package/dist/lib/blockers.d.ts +66 -0
  116. package/dist/lib/blockers.js +103 -0
  117. package/dist/lib/blockers.js.map +1 -0
  118. package/dist/lib/claims.d.ts +39 -0
  119. package/dist/lib/claims.js +133 -0
  120. package/dist/lib/claims.js.map +1 -0
  121. package/dist/lib/classify-debt.d.ts +17 -0
  122. package/dist/lib/classify-debt.js +13 -0
  123. package/dist/lib/classify-debt.js.map +1 -0
  124. package/dist/lib/code-evidence.d.ts +35 -0
  125. package/dist/lib/code-evidence.js +171 -0
  126. package/dist/lib/code-evidence.js.map +1 -0
  127. package/dist/lib/coherence.d.ts +14 -0
  128. package/dist/lib/coherence.js +188 -0
  129. package/dist/lib/coherence.js.map +1 -0
  130. package/dist/lib/completion-gate.d.ts +17 -0
  131. package/dist/lib/completion-gate.js +28 -0
  132. package/dist/lib/completion-gate.js.map +1 -0
  133. package/dist/lib/compliance-signoff.d.ts +39 -0
  134. package/dist/lib/compliance-signoff.js +81 -0
  135. package/dist/lib/compliance-signoff.js.map +1 -0
  136. package/dist/lib/config.d.ts +63 -0
  137. package/dist/lib/config.js +40 -0
  138. package/dist/lib/config.js.map +1 -0
  139. package/dist/lib/cost.d.ts +12 -0
  140. package/dist/lib/cost.js +12 -0
  141. package/dist/lib/cost.js.map +1 -0
  142. package/dist/lib/debt-dashboard.d.ts +25 -0
  143. package/dist/lib/debt-dashboard.js +36 -0
  144. package/dist/lib/debt-dashboard.js.map +1 -0
  145. package/dist/lib/embedding-store.d.ts +103 -0
  146. package/dist/lib/embedding-store.js +133 -0
  147. package/dist/lib/embedding-store.js.map +1 -0
  148. package/dist/lib/embeddings-source.d.ts +36 -0
  149. package/dist/lib/embeddings-source.js +62 -0
  150. package/dist/lib/embeddings-source.js.map +1 -0
  151. package/dist/lib/enforcement-allowlist.d.ts +12 -0
  152. package/dist/lib/enforcement-allowlist.js +25 -0
  153. package/dist/lib/enforcement-allowlist.js.map +1 -0
  154. package/dist/lib/events.d.ts +47 -0
  155. package/dist/lib/events.js +75 -0
  156. package/dist/lib/events.js.map +1 -0
  157. package/dist/lib/exec.d.ts +8 -0
  158. package/dist/lib/exec.js +25 -0
  159. package/dist/lib/exec.js.map +1 -0
  160. package/dist/lib/frontmatter.d.ts +112 -0
  161. package/dist/lib/frontmatter.js +261 -0
  162. package/dist/lib/frontmatter.js.map +1 -0
  163. package/dist/lib/funnel.d.ts +22 -0
  164. package/dist/lib/funnel.js +41 -0
  165. package/dist/lib/funnel.js.map +1 -0
  166. package/dist/lib/git-mtime.d.ts +4 -0
  167. package/dist/lib/git-mtime.js +23 -0
  168. package/dist/lib/git-mtime.js.map +1 -0
  169. package/dist/lib/metrics.d.ts +42 -0
  170. package/dist/lib/metrics.js +92 -0
  171. package/dist/lib/metrics.js.map +1 -0
  172. package/dist/lib/migration-collision.d.ts +20 -0
  173. package/dist/lib/migration-collision.js +61 -0
  174. package/dist/lib/migration-collision.js.map +1 -0
  175. package/dist/lib/next-action.d.ts +44 -0
  176. package/dist/lib/next-action.js +138 -0
  177. package/dist/lib/next-action.js.map +1 -0
  178. package/dist/lib/ollama-embed.d.ts +21 -0
  179. package/dist/lib/ollama-embed.js +65 -0
  180. package/dist/lib/ollama-embed.js.map +1 -0
  181. package/dist/lib/output.d.ts +35 -0
  182. package/dist/lib/output.js +46 -0
  183. package/dist/lib/output.js.map +1 -0
  184. package/dist/lib/pipeline-config.d.ts +20 -0
  185. package/dist/lib/pipeline-config.js +208 -0
  186. package/dist/lib/pipeline-config.js.map +1 -0
  187. package/dist/lib/pipeline-map.d.ts +68 -0
  188. package/dist/lib/pipeline-map.js +256 -0
  189. package/dist/lib/pipeline-map.js.map +1 -0
  190. package/dist/lib/pipeline-schema.d.ts +48 -0
  191. package/dist/lib/pipeline-schema.js +22 -0
  192. package/dist/lib/pipeline-schema.js.map +1 -0
  193. package/dist/lib/placement.d.ts +6 -0
  194. package/dist/lib/placement.js +40 -0
  195. package/dist/lib/placement.js.map +1 -0
  196. package/dist/lib/precedent-search.d.ts +18 -0
  197. package/dist/lib/precedent-search.js +160 -0
  198. package/dist/lib/precedent-search.js.map +1 -0
  199. package/dist/lib/predicate-registry.d.ts +6 -0
  200. package/dist/lib/predicate-registry.js +40 -0
  201. package/dist/lib/predicate-registry.js.map +1 -0
  202. package/dist/lib/recall-cases.d.ts +15 -0
  203. package/dist/lib/recall-cases.js +39 -0
  204. package/dist/lib/recall-cases.js.map +1 -0
  205. package/dist/lib/repo.d.ts +20 -0
  206. package/dist/lib/repo.js +82 -0
  207. package/dist/lib/repo.js.map +1 -0
  208. package/dist/lib/requirements.d.ts +91 -0
  209. package/dist/lib/requirements.js +179 -0
  210. package/dist/lib/requirements.js.map +1 -0
  211. package/dist/lib/rollup.d.ts +24 -0
  212. package/dist/lib/rollup.js +34 -0
  213. package/dist/lib/rollup.js.map +1 -0
  214. package/dist/lib/scorecard-run.d.ts +6 -0
  215. package/dist/lib/scorecard-run.js +48 -0
  216. package/dist/lib/scorecard-run.js.map +1 -0
  217. package/dist/lib/scorecard.d.ts +69 -0
  218. package/dist/lib/scorecard.js +113 -0
  219. package/dist/lib/scorecard.js.map +1 -0
  220. package/dist/lib/semantic-search.d.ts +36 -0
  221. package/dist/lib/semantic-search.js +56 -0
  222. package/dist/lib/semantic-search.js.map +1 -0
  223. package/dist/lib/session-state.d.ts +8 -0
  224. package/dist/lib/session-state.js +25 -0
  225. package/dist/lib/session-state.js.map +1 -0
  226. package/dist/lib/spec-debt.d.ts +8 -0
  227. package/dist/lib/spec-debt.js +17 -0
  228. package/dist/lib/spec-debt.js.map +1 -0
  229. package/dist/lib/spec-pure.d.ts +59 -0
  230. package/dist/lib/spec-pure.js +215 -0
  231. package/dist/lib/spec-pure.js.map +1 -0
  232. package/dist/lib/specs.d.ts +148 -0
  233. package/dist/lib/specs.js +373 -0
  234. package/dist/lib/specs.js.map +1 -0
  235. package/dist/lib/test-generator.d.ts +5 -0
  236. package/dist/lib/test-generator.js +72 -0
  237. package/dist/lib/test-generator.js.map +1 -0
  238. package/dist/lib/user-stories.d.ts +6 -0
  239. package/dist/lib/user-stories.js +56 -0
  240. package/dist/lib/user-stories.js.map +1 -0
  241. package/dist/lib/verifies-scan.d.ts +9 -0
  242. package/dist/lib/verifies-scan.js +73 -0
  243. package/dist/lib/verifies-scan.js.map +1 -0
  244. package/dist/lib/verify-core.d.ts +23 -0
  245. package/dist/lib/verify-core.js +48 -0
  246. package/dist/lib/verify-core.js.map +1 -0
  247. package/dist/lib/with-event-log.d.ts +12 -0
  248. package/dist/lib/with-event-log.js +65 -0
  249. package/dist/lib/with-event-log.js.map +1 -0
  250. package/dist/lib/wizard-specs.d.ts +17 -0
  251. package/dist/lib/wizard-specs.js +34 -0
  252. package/dist/lib/wizard-specs.js.map +1 -0
  253. package/dist/pure.d.ts +19 -0
  254. package/dist/pure.js +31 -0
  255. package/dist/pure.js.map +1 -0
  256. package/package.json +69 -0
@@ -0,0 +1,373 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { parseComplianceSignoff } from './compliance-signoff.js';
4
+ import { readFrontmatter, stripFrontmatter } from './frontmatter.js';
5
+ import { coresList, specsRoot } from './repo.js';
6
+ import { deriveSignalsFromText, extractSpecId, isDerivativeArtifact, isLifecycleArtifact, isPhaseLifecycleFile, } from './spec-pure.js';
7
+ import { getWizardCreateSpec } from './wizard-specs.js';
8
+ // Path classification lives in the fs-free spec-pure.ts (single source); re-export
9
+ // the public helpers so existing importers of specs.ts (reconcile, placement) keep
10
+ // working. `extractSpecId` is core-anchored — handles encoreos's 2-char cores AND
11
+ // builder's longer cores (RUNNER) with no over-match (epic #300, Wave-1 G3).
12
+ export { extractSpecId, isDerivativeArtifact };
13
+ /** The marker the generator emits for an AC-less spec (mirrors
14
+ * scripts/uat/generate-playwright-from-spec.ts NO_AC_MARKER). Cannot import
15
+ * across the eos-spec/scripts boundary (rootDir: ./src), so the literal is duplicated. */
16
+ const GENERATED_NO_AC_MARKER = 'no acceptance_criteria — run encore-spec-ac-draft first';
17
+ /** True when a generated test source has ≥1 ACTIVE `test(` block (not `test.fixme`/
18
+ * `test.describe`) and is not an AC-less placeholder. Mirrors the active-test regex
19
+ * in scripts/specs/check-generated-tests.ts (flagWeakTest): the negative lookbehind
20
+ * rejects `test.fixme(`, `test.describe(`, and `/re/.test(`. Line comments are stripped
21
+ * first so a `test(` mentioned in a `//` comment is not a false positive (the canonical
22
+ * regex runs on real generated files, which never carry such comments). */
23
+ export function hasActiveGeneratedTestSource(source) {
24
+ if (source.includes(GENERATED_NO_AC_MARKER))
25
+ return false;
26
+ const withoutLineComments = source.replace(/\/\/[^\n]*/g, '');
27
+ return /(?<![.\w])test\s*\(/.test(withoutLineComments);
28
+ }
29
+ /** Cores requiring regulatory grounding + compliance review (matches scripts/specs/validate-compliance-gate.ts). */
30
+ export const REGULATED_CORES = new Set(['cl', 'pm', 'hr', 'rh', 'gr']);
31
+ /**
32
+ * Derive pipeline signals from a spec's core + raw text. File-existence flags are
33
+ * supplied by the caller. Thin positional-args adapter over the pure
34
+ * {@link deriveSignalsFromText} (spec-pure.ts) — the single source of signal
35
+ * derivation, shared with browser consumers via `/pure`. Keep this signature
36
+ * stable; route new signal logic through `deriveSignalsFromText`.
37
+ */
38
+ export function deriveSignals(core, text, researchFileExists, reviewFileExists = false, generatedTestExists = false, complianceSignoffFileExists = false, docshotImageExists = false, activeGeneratedTests = false, isCreateWizardSpec = false, submitE2eProvesPersist = false, linkedLocalTestExists = false) {
39
+ return deriveSignalsFromText(core, text, {
40
+ researchFileExists,
41
+ reviewFileExists,
42
+ generatedTestExists,
43
+ complianceSignoffFileExists,
44
+ docshotImageExists,
45
+ activeGeneratedTests,
46
+ isCreateWizardSpec,
47
+ submitE2eProvesPersist,
48
+ linkedLocalTestExists,
49
+ });
50
+ }
51
+ const STATUS_REGEX = /\*\*Status:\*\*\s*([^\n]+)/i;
52
+ const TITLE_REGEX = /^#\s+(.+)$/m;
53
+ export function listSpecs(repoRoot, coreFilter, options) {
54
+ const out = [];
55
+ const includeArchived = Boolean(options?.includeArchived);
56
+ const cores = coreFilter ? [coreFilter.toLowerCase()] : coresList(repoRoot);
57
+ for (const core of cores) {
58
+ const activeDir = join(specsRoot(repoRoot), core, 'specs');
59
+ if (existsSync(activeDir)) {
60
+ collectSpecsFromDirectory(repoRoot, core, activeDir, false, out);
61
+ }
62
+ const uxDir = join(specsRoot(repoRoot), core, 'ux');
63
+ if (existsSync(uxDir)) {
64
+ collectSpecsFromDirectory(repoRoot, core, uxDir, false, out);
65
+ }
66
+ if (includeArchived) {
67
+ const archiveDir = join(specsRoot(repoRoot), core, 'archive');
68
+ if (existsSync(archiveDir)) {
69
+ collectSpecsFromDirectory(repoRoot, core, archiveDir, true, out);
70
+ }
71
+ }
72
+ }
73
+ return out;
74
+ }
75
+ function collectSpecsFromDirectory(repoRoot, core, dir, archived, out) {
76
+ for (const file of readdirSync(dir)) {
77
+ if (!file.endsWith('.md'))
78
+ continue;
79
+ if (file.includes('CONTEXT'))
80
+ continue;
81
+ if (file.startsWith('SPEC_INDEX'))
82
+ continue;
83
+ if (file.startsWith('README'))
84
+ continue;
85
+ if (file.includes('ENHANCEMENTS'))
86
+ continue;
87
+ if (file.includes('EXPANSION'))
88
+ continue;
89
+ if (isPhaseLifecycleFile(file))
90
+ continue;
91
+ if (isLifecycleArtifact(file))
92
+ continue;
93
+ const id = extractSpecId(file, core);
94
+ if (!id)
95
+ continue;
96
+ const path = join(dir, file);
97
+ let title;
98
+ let status;
99
+ let supersededBy;
100
+ let text = '';
101
+ try {
102
+ text = readFileSync(path, 'utf8');
103
+ // Frontmatter wins; body prose (`# Heading`, `**Status:**`) is fallback only.
104
+ // Body regexes run on the frontmatter-stripped text so the SPEC_TEMPLATE
105
+ // YAML comment (`# Replace placeholder values when authoring.`) can never
106
+ // be mistaken for the H1 title.
107
+ const fm = readFrontmatter(text);
108
+ const body = stripFrontmatter(text);
109
+ const fmTitle = typeof fm.title === 'string' && fm.title.trim() !== '' ? fm.title.trim() : undefined;
110
+ const fmStatus = typeof fm.status === 'string' && fm.status.trim() !== '' ? fm.status.trim() : undefined;
111
+ title = fmTitle ?? TITLE_REGEX.exec(body)?.[1]?.trim();
112
+ status = fmStatus ?? STATUS_REGEX.exec(body)?.[1]?.trim();
113
+ supersededBy =
114
+ typeof fm.superseded_by === 'string' && fm.superseded_by.trim() !== '' ? fm.superseded_by.trim() : undefined;
115
+ }
116
+ catch {
117
+ // ignore
118
+ }
119
+ const researchFileExists = existsSync(join(specsRoot(repoRoot), core, 'research', `${id}-RESEARCH.md`));
120
+ const reviewFileExists = hasReviewFile(repoRoot, core, id);
121
+ const generatedTestExists = generatedTestExistsFor(repoRoot, core, id);
122
+ const activeGeneratedTests = hasActiveGeneratedTestSource(generatedTestSourceFor(repoRoot, core, id));
123
+ const linkedLocalTestExists = linkedLocalTestFor(repoRoot, core, id);
124
+ const complianceSignoffFileExists = hasComplianceSignoffFile(repoRoot, core, id);
125
+ const docshotImageExists = docshotImageExistsFor(repoRoot, id);
126
+ const wizardCreate = getWizardCreateSpec(repoRoot, id);
127
+ const isCreateWizardSpec = wizardCreate?.kind === 'create';
128
+ const submitE2eProvesPersist = isCreateWizardSpec
129
+ ? submitE2eProvesPersistFor(repoRoot, core, id, wizardCreate.targetTable)
130
+ : false;
131
+ out.push({
132
+ id,
133
+ core,
134
+ path,
135
+ title,
136
+ status,
137
+ supersededBy,
138
+ hasPlan: hasArtifact(repoRoot, core, 'plans', id, 'PLAN'),
139
+ hasTasks: hasArtifact(repoRoot, core, 'tasks', id, 'TASKS'),
140
+ hasContext: existsSync(join(specsRoot(repoRoot), core, 'specs', `${id}-CONTEXT.md`)),
141
+ signals: deriveSignals(core, text, researchFileExists, reviewFileExists, generatedTestExists, complianceSignoffFileExists, docshotImageExists, activeGeneratedTests, isCreateWizardSpec, submitE2eProvesPersist, linkedLocalTestExists),
142
+ archived,
143
+ });
144
+ }
145
+ }
146
+ /**
147
+ * Resolve the on-disk path of a spec artifact (plan/tasks) for `id`, or null when none exists.
148
+ * Folder-agnostic: the artifact dir is `specs/{core}/{folder}/`, independent of whether the
149
+ * spec itself lives under `specs/{core}/specs/` or `specs/{core}/ux/` — which is exactly why
150
+ * the old `/\/specs\/[^/]+\.md$/` path-derivation regex in next.ts silently failed for UX specs.
151
+ */
152
+ export function resolveArtifactPath(repoRoot, core, folder, id, suffix) {
153
+ const artifactDir = join(specsRoot(repoRoot), core, folder);
154
+ if (!existsSync(artifactDir))
155
+ return null;
156
+ const match = readdirSync(artifactDir).find((file) => {
157
+ if (file === `${id}-${suffix}.md`)
158
+ return true;
159
+ if (!file.startsWith(`${id}-`) || !file.endsWith(`-${suffix}.md`))
160
+ return false;
161
+ // A child sub-spec's artifact (PF-112-EN-01-TASKS.md, PF-99-P2-TASKS.md)
162
+ // must not credit the parent id — that prefix collision made PF-112
163
+ // compute tasks_generated off its enhancement's tasks file while the
164
+ // strict per-file gate (artifacts.tasks path) correctly said no.
165
+ const middle = file.slice(id.length + 1, file.length - `-${suffix}.md`.length);
166
+ return !/^(EN-\d+|P\d+)(-|$)/i.test(middle);
167
+ });
168
+ return match ? join(artifactDir, match) : null;
169
+ }
170
+ function hasArtifact(repoRoot, core, folder, id, suffix) {
171
+ return resolveArtifactPath(repoRoot, core, folder, id, suffix) !== null;
172
+ }
173
+ /** A saved spec-review report may live in flat `specs/reviews/` or per-core `specs/{core}/reviews/`,
174
+ * with names like `{ID}-SPEC-REVIEW.md` or combined `{ID-A}-{ID-B}-SPEC-REVIEW.md`. */
175
+ function hasReviewFile(repoRoot, core, id) {
176
+ const dirs = [join(specsRoot(repoRoot), 'reviews'), join(specsRoot(repoRoot), core, 'reviews')];
177
+ const idAtBoundary = new RegExp(`(^|-)${id}(-|\\.)`, 'i');
178
+ for (const dir of dirs) {
179
+ if (!existsSync(dir))
180
+ continue;
181
+ if (readdirSync(dir).some((file) => /SPEC-REVIEW/i.test(file) && idAtBoundary.test(file)))
182
+ return true;
183
+ }
184
+ return false;
185
+ }
186
+ /** Compliance sign-off lives in flat `specs/reviews/` or per-core `specs/{core}/reviews/`,
187
+ * named like `{ID}-COMPLIANCE-SIGNOFF.md` (also `{ID}-...-COMPLIANCE-SIGNOFF.md`).
188
+ * C3: a file now counts ONLY when its frontmatter parses as a valid sign-off
189
+ * (matching spec_id + reviewer + ISO date + >=1 known-regime determination).
190
+ * The legacy bare `<!-- compliance-reviewed: -->` body comment remains a separate,
191
+ * documented escape path handled in `deriveSignals` — this function gates the FILE only. */
192
+ function hasComplianceSignoffFile(repoRoot, core, id) {
193
+ const dirs = [join(specsRoot(repoRoot), 'reviews'), join(specsRoot(repoRoot), core, 'reviews')];
194
+ const idAtBoundary = new RegExp(`(^|-)${id}(-|\\.)`, 'i');
195
+ for (const dir of dirs) {
196
+ if (!existsSync(dir))
197
+ continue;
198
+ for (const file of readdirSync(dir)) {
199
+ if (!/COMPLIANCE-SIGNOFF/i.test(file) || !idAtBoundary.test(file))
200
+ continue;
201
+ try {
202
+ const body = readFileSync(join(dir, file), 'utf8');
203
+ if (parseComplianceSignoff(body, id).valid)
204
+ return true;
205
+ }
206
+ catch {
207
+ // unreadable file: treat as not a valid sign-off
208
+ }
209
+ }
210
+ }
211
+ return false;
212
+ }
213
+ /** Checks whether a generated Playwright test file exists on disk for this spec.
214
+ * Generated tests live at `tests/e2e/generated/<core>/<id-lowercase>-<slug>.generated.spec.ts`. */
215
+ function generatedTestExistsFor(repoRoot, core, id) {
216
+ const dir = join(repoRoot, 'tests', 'e2e', 'generated', core.toLowerCase());
217
+ if (!existsSync(dir))
218
+ return false;
219
+ const prefix = `${id.toLowerCase()}-`;
220
+ return readdirSync(dir).some((f) => f.startsWith(prefix) && f.endsWith('.generated.spec.ts'));
221
+ }
222
+ /** Read the generated Playwright test source for a spec, or '' if none on disk.
223
+ * Path: tests/e2e/generated/<core>/<id-lower>-<slug>.generated.spec.ts. */
224
+ function generatedTestSourceFor(repoRoot, core, id) {
225
+ const dir = join(repoRoot, 'tests', 'e2e', 'generated', core.toLowerCase());
226
+ if (!existsSync(dir))
227
+ return '';
228
+ const prefix = `${id.toLowerCase()}-`;
229
+ const file = readdirSync(dir).find((f) => f.startsWith(prefix) && f.endsWith('.generated.spec.ts'));
230
+ return file ? readFileSync(join(dir, file), 'utf8') : '';
231
+ }
232
+ /** True when a tests/e2e/<core>/<id-lower>(-<slug>)?-local.spec.ts exists AND
233
+ * proves a persisted row was *read back* after submit: it imports createClient AND
234
+ * contains a `.from('<targetTable>')` chained to a `.select(` — the post-submit
235
+ * verification read — not merely a `.from('<targetTable>')` that could be a
236
+ * setup/cleanup `.delete()` (#1327). Quote style is free (`'`, `"`, or backtick),
237
+ * and arbitrary whitespace/newlines/chaining is allowed between the `.from(...)`
238
+ * and the `.select(` (the Supabase builder is fluent, so the read-back commonly
239
+ * spans lines: `db\n .from('t')\n .select(...)`). The submit→DB completion signal.
240
+ * The repo convention is `<id>-<slug>-local.spec.ts` (e.g.
241
+ * gr-ux-10-contract-creation-local.spec.ts), so the file is matched by prefix —
242
+ * the same prefix-glob `generatedTestExistsFor` uses — not an exact filename. */
243
+ export function submitE2eProvesPersistFor(repoRoot, core, id, targetTable) {
244
+ const dir = join(repoRoot, 'tests', 'e2e', core.toLowerCase());
245
+ if (!existsSync(dir))
246
+ return false;
247
+ const prefix = `${id.toLowerCase()}-`;
248
+ // `.from('<table>')` immediately followed (only whitespace/newlines between) by a
249
+ // chained `.select(` — the read-back. A bare `.from('<table>').delete()` cleanup no
250
+ // longer satisfies the signal. `[\s\S]` is intentionally not used between `.from`
251
+ // and `.select` so an unrelated later `.delete()` cannot be glued to an earlier read.
252
+ const readsBackTarget = new RegExp(`\\.from\\(\\s*['"\`]${targetTable}['"\`]\\s*\\)\\s*\\.select\\(`);
253
+ for (const file of readdirSync(dir)) {
254
+ if (!file.toLowerCase().startsWith(prefix) || !file.endsWith('-local.spec.ts'))
255
+ continue;
256
+ let text = '';
257
+ try {
258
+ text = readFileSync(join(dir, file), 'utf8');
259
+ }
260
+ catch {
261
+ continue;
262
+ }
263
+ if (/createClient/.test(text) && readsBackTarget.test(text))
264
+ return true;
265
+ }
266
+ return false;
267
+ }
268
+ /** True when a tests/e2e/<core>/<id>(-slug)?-local.spec.ts exists whose source
269
+ * carries a `@verifies <ID>` backlink — the hand-authored E2E path. */
270
+ function linkedLocalTestFor(repoRoot, core, id) {
271
+ const dir = join(repoRoot, 'tests', 'e2e', core.toLowerCase());
272
+ if (!existsSync(dir))
273
+ return false;
274
+ const prefix = `${id.toLowerCase()}-`;
275
+ const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
276
+ const verifies = new RegExp(`@verifies\\s+${escaped}\\b`, 'i');
277
+ for (const file of readdirSync(dir)) {
278
+ if (!file.toLowerCase().startsWith(prefix) || !file.endsWith('-local.spec.ts'))
279
+ continue;
280
+ try {
281
+ if (verifies.test(readFileSync(join(dir, file), 'utf8')))
282
+ return true;
283
+ }
284
+ catch {
285
+ // unreadable: skip
286
+ }
287
+ }
288
+ return false;
289
+ }
290
+ /**
291
+ * All specs whose ID matches `specId` (case-insensitive) across `specs/` and
292
+ * `ux/` — plus `archive/` when `options.includeArchived` is set. Duplicate spec
293
+ * IDs are live in the corpus today (e.g. PF-76, PF-77); callers that
294
+ * gate on identity (create/archive collision checks, the navigator) must use
295
+ * this instead of `findSpec` so collisions are visible, never silently resolved.
296
+ */
297
+ export function findSpecsById(repoRoot, specId, options) {
298
+ const upper = specId.toUpperCase();
299
+ const core = upper.split('-')[0].toLowerCase();
300
+ return listSpecs(repoRoot, core, options).filter((s) => s.id.toUpperCase() === upper);
301
+ }
302
+ export function findSpec(repoRoot, specId, options) {
303
+ const matches = findSpecsById(repoRoot, specId, options);
304
+ if (matches.length === 0)
305
+ return null;
306
+ if (matches.length > 1) {
307
+ // stderr, not stdout, so `--json` consumers of findSpec-backed commands keep clean machine output.
308
+ process.stderr.write(`warning: ${matches.length} specs share ID ${matches[0].id}; using ${matches[0].path}\n`);
309
+ }
310
+ return matches[0];
311
+ }
312
+ /**
313
+ * When findSpec returns null, look for a markdown file on disk whose name starts
314
+ * with the id so we can give honest advice instead of always suggesting
315
+ * create-spec. Walks `specs/{core}/` recursively.
316
+ */
317
+ export function diagnoseMissingSpec(repoRoot, specId) {
318
+ const upper = specId.toUpperCase();
319
+ const core = upper.split('-')[0].toLowerCase();
320
+ const coreDir = join(specsRoot(repoRoot), core);
321
+ if (!existsSync(coreDir)) {
322
+ return { reason: `no specs directory for core "${core}"` };
323
+ }
324
+ const stack = [coreDir];
325
+ while (stack.length > 0) {
326
+ const dir = stack.pop();
327
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
328
+ const full = join(dir, entry.name);
329
+ if (entry.isDirectory()) {
330
+ stack.push(full);
331
+ }
332
+ else if (entry.name.endsWith('.md') && entry.name.toUpperCase().startsWith(`${upper}-`)) {
333
+ const inScanned = /[\\/](?:specs|ux)[\\/]/.test(full);
334
+ return {
335
+ foundPath: full,
336
+ reason: inScanned
337
+ ? 'file exists in a scanned location but its name was unparseable'
338
+ : 'file exists but is not in a scanned location (specs/ or ux/)',
339
+ };
340
+ }
341
+ }
342
+ }
343
+ return { reason: 'no file matching this id was found on disk' };
344
+ }
345
+ /** True when a code-sourced docshot for the spec exists under docs/images/<area>/.
346
+ * Shots follow the `<spec-id-lowercase>-*.png` naming convention (CE-62/69/74…);
347
+ * the area folder is usually the core but not always (e.g. ats/ for HR specs),
348
+ * so every area subdirectory is scanned. Cached per process — listSpecs walks
349
+ * the whole corpus and the image tree is identical for every spec. */
350
+ let docshotIndexCache = null;
351
+ function docshotImageExistsFor(repoRoot, id) {
352
+ if (docshotIndexCache === null) {
353
+ docshotIndexCache = new Set();
354
+ const imagesRoot = join(repoRoot, 'docs', 'images');
355
+ if (existsSync(imagesRoot)) {
356
+ for (const area of readdirSync(imagesRoot, { withFileTypes: true })) {
357
+ if (!area.isDirectory())
358
+ continue;
359
+ for (const file of readdirSync(join(imagesRoot, area.name))) {
360
+ if (file.endsWith('.png'))
361
+ docshotIndexCache.add(file.toLowerCase());
362
+ }
363
+ }
364
+ }
365
+ }
366
+ const prefix = `${id.toLowerCase()}-`;
367
+ for (const file of docshotIndexCache) {
368
+ if (file.startsWith(prefix))
369
+ return true;
370
+ }
371
+ return false;
372
+ }
373
+ //# sourceMappingURL=specs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"specs.js","sourceRoot":"","sources":["../../src/lib/specs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { AcceptanceCriterion } from './requirements.js';
2
+ export type Lane = 'ui' | 'integration';
3
+ export declare function classifyLane(acs: AcceptanceCriterion[]): Lane;
4
+ export declare function generatedRelPath(specId: string, lane: Lane): string;
5
+ export declare function generateTestContent(specId: string, title: string, acs: AcceptanceCriterion[], lane: Lane): string;
@@ -0,0 +1,72 @@
1
+ const UI_KEYWORDS = [
2
+ 'page',
3
+ 'click',
4
+ 'navigate',
5
+ 'redirect',
6
+ 'button',
7
+ 'dialog',
8
+ 'visible',
9
+ 'screen',
10
+ 'form',
11
+ 'render',
12
+ 'link',
13
+ 'display',
14
+ 'ui',
15
+ 'modal',
16
+ 'toast',
17
+ ];
18
+ export function classifyLane(acs) {
19
+ const hay = acs
20
+ .flatMap((a) => [...a.given, ...a.when, ...a.then])
21
+ .join(' ')
22
+ .toLowerCase();
23
+ return UI_KEYWORDS.some((k) => new RegExp(`\\b${k}\\b`).test(hay)) ? 'ui' : 'integration';
24
+ }
25
+ export function generatedRelPath(specId, lane) {
26
+ const slug = specId.toLowerCase();
27
+ return lane === 'ui' ? `e2e/generated/${slug}.generated.spec.ts` : `e2e/generated/${slug}.generated.int.test.ts`;
28
+ }
29
+ function steps(ac) {
30
+ const lines = [
31
+ ...ac.given.map((g) => `given: ${g}`),
32
+ ...ac.when.map((w) => `when: ${w}`),
33
+ ...ac.then.map((t) => `then: ${t}`),
34
+ ];
35
+ return lines
36
+ .map((l) => ` await test.step(${JSON.stringify(l)}, async () => {\n // TODO: implement\n });`)
37
+ .join('\n');
38
+ }
39
+ function uiBlock(specId, ac) {
40
+ const fn = ac.statementOnly ? 'test.fixme' : 'test';
41
+ const title = `${ac.id}: ${[...ac.when].join('; ') || 'statement-only'}`;
42
+ const body = ac.statementOnly
43
+ ? ' // statement-only AC — flesh out when implemented'
44
+ : `${steps(ac)}\n expect(true).toBe(true); // TODO: real assertion`;
45
+ return [
46
+ `/** @verifies ${specId} ${ac.id} */`,
47
+ `${fn}(${JSON.stringify(title)}, async ({ page }) => {`,
48
+ body,
49
+ `});`,
50
+ ].join('\n');
51
+ }
52
+ function uiFile(specId, title, acs) {
53
+ const safeTitle = title.replace(/\s*\n\s*/g, ' ').trim();
54
+ const header = `// GENERATED by \`eos-spec gen-tests\` for ${specId} — ${safeTitle}. Fixme-gated skeleton; flesh out.\nimport { test, expect } from "../fixtures/console-signals.fixture";\n`;
55
+ if (acs.length === 0)
56
+ return `${header}\ntest.fixme(${JSON.stringify(`${specId} has no acceptance criteria yet`)}, async () => {});\n`;
57
+ return `${header}\n${acs.map((a) => uiBlock(specId, a)).join('\n\n')}\n`;
58
+ }
59
+ function intFile(specId, title, acs) {
60
+ const safeTitle = title.replace(/\s*\n\s*/g, ' ').trim();
61
+ const header = `// GENERATED by \`eos-spec gen-tests\` for ${specId} — ${safeTitle}. Stub; flesh out.\nimport { describe, it } from "vitest";\n`;
62
+ const body = acs.length === 0
63
+ ? ` it.todo(${JSON.stringify(`${specId} has no acceptance criteria yet`)});`
64
+ : acs
65
+ .map((a) => ` // @verifies ${specId} ${a.id}\n it.todo(${JSON.stringify(`${a.id}: ${[...a.then].join('; ') || 'statement-only'}`)});`)
66
+ .join('\n');
67
+ return `${header}\ndescribe(${JSON.stringify(specId)}, () => {\n${body}\n});\n`;
68
+ }
69
+ export function generateTestContent(specId, title, acs, lane) {
70
+ return lane === 'ui' ? uiFile(specId, title, acs) : intFile(specId, title, acs);
71
+ }
72
+ //# sourceMappingURL=test-generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-generator.js","sourceRoot":"","sources":["../../src/lib/test-generator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { UserStory } from './ac-draft-prompt.js';
2
+ /** Extract user stories from a spec body, supporting both the bullet format
3
+ * (`- **US-1**: text`) and the markdown-header format used by UX/wizard specs
4
+ * (`### US-1: Title` followed by `**As a** … **I want** … **So that** …`).
5
+ * Bullet matches win when an id appears in both forms. Returns one entry per id. */
6
+ export declare function extractUserStories(body: string): UserStory[];
@@ -0,0 +1,56 @@
1
+ // Bullet format: `- **US-1**: text` / `* US-2 text` (story text on the same line).
2
+ const BULLET_RE = /^[-*]\s*\*\*?(US-\d+[\w.-]*)\*?\*?:?\s*(.+)$/gim;
3
+ // Header format: `### US-1: Title` (UX/wizard specs); role lines follow underneath.
4
+ const HEADER_RE = /^#{2,4}\s+(US-\d+[\w.-]*)\s*:?\s*(.*)$/;
5
+ const ROLE_RE = /^\*\*(As a|I want|So that)\*\*/i;
6
+ /** Extract user stories from a spec body, supporting both the bullet format
7
+ * (`- **US-1**: text`) and the markdown-header format used by UX/wizard specs
8
+ * (`### US-1: Title` followed by `**As a** … **I want** … **So that** …`).
9
+ * Bullet matches win when an id appears in both forms. Returns one entry per id. */
10
+ export function extractUserStories(body) {
11
+ const byId = new Map();
12
+ for (const m of body.matchAll(BULLET_RE)) {
13
+ const id = m[1];
14
+ const text = m[2].trim();
15
+ if (text && !byId.has(id))
16
+ byId.set(id, text);
17
+ }
18
+ const lines = body.split(/\r?\n/);
19
+ for (let i = 0; i < lines.length; i++) {
20
+ const h = HEADER_RE.exec(lines[i]);
21
+ if (!h)
22
+ continue;
23
+ const id = h[1];
24
+ if (byId.has(id))
25
+ continue;
26
+ const parts = [];
27
+ const title = h[2].trim();
28
+ if (title)
29
+ parts.push(title);
30
+ let seenRole = false;
31
+ for (let j = i + 1; j < lines.length; j++) {
32
+ const line = lines[j].trim();
33
+ // Allow blank lines before the role block starts, but stop once the
34
+ // contiguous role block ends (a blank line after at least one role line).
35
+ if (line === '') {
36
+ if (seenRole)
37
+ break;
38
+ continue;
39
+ }
40
+ if (/^#{1,6}\s/.test(line))
41
+ break;
42
+ if (ROLE_RE.test(line)) {
43
+ parts.push(line.replace(/\*\*/g, '').trim());
44
+ seenRole = true;
45
+ }
46
+ else if (seenRole) {
47
+ break;
48
+ }
49
+ }
50
+ const text = parts.join(' — ').trim();
51
+ if (text)
52
+ byId.set(id, text);
53
+ }
54
+ return [...byId.entries()].map(([id, text]) => ({ id, text }));
55
+ }
56
+ //# sourceMappingURL=user-stories.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-stories.js","sourceRoot":"","sources":["../../src/lib/user-stories.ts"],"names":[],"mappings":"AAEA,mFAAmF;AACnF,MAAM,SAAS,GAAG,iDAAiD,CAAC;AACpE,oFAAoF;AACpF,MAAM,SAAS,GAAG,wCAAwC,CAAC;AAC3D,MAAM,OAAO,GAAG,iCAAiC,CAAC;AAElD;;;qFAGqF;AACrF,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;IAEvC,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QACzC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;QACjB,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAC;QAC1B,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IAChD,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAClC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC;QACpC,IAAI,CAAC,CAAC;YAAE,SAAS;QACjB,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;QACjB,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,SAAS;QAC3B,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAC;QAC3B,IAAI,KAAK;YAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC7B,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,KAAK,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC1C,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAC;YAC9B,oEAAoE;YACpE,0EAA0E;YAC1E,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;gBAChB,IAAI,QAAQ;oBAAE,MAAM;gBACpB,SAAS;YACX,CAAC;YACD,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,MAAM;YAClC,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACvB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;gBAC7C,QAAQ,GAAG,IAAI,CAAC;YAClB,CAAC;iBAAM,IAAI,QAAQ,EAAE,CAAC;gBACpB,MAAM;YACR,CAAC;QACH,CAAC;QACD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;QACtC,IAAI,IAAI;YAAE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IAC/B,CAAC;IAED,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AACjE,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Parse `@verifies <SPEC> <AC/US ids…>` backlinks from one file's text.
3
+ * Matches scripts/specs/verify-acs.mjs semantics: a spec id followed by a run of AC-/US- ids.
4
+ * The spec-id group is non-greedy so compound ids (e.g. PF-96-EN-01) are captured whole
5
+ * while the trailing AC/US run is still attributed correctly.
6
+ */
7
+ export declare function parseVerifies(fileText: string): Map<string, Set<string>>;
8
+ /** Walk the repo's test trees and build specId → Set(verified AC/US ids). */
9
+ export declare function scanVerifies(repoRoot: string, roots?: string[]): Map<string, Set<string>>;
@@ -0,0 +1,73 @@
1
+ import { readdirSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ const TEST_FILE = /\.(test|spec)\.tsx?$/;
4
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage']);
5
+ /**
6
+ * Test trees that exercise the verification tooling ITSELF — their `@verifies` strings are
7
+ * fixture literals, not evidence. tests/unit/specs/verify-acs.test.ts was being counted as
8
+ * real FM verification, corrupting the FM and platform scorecard headlines. Scoped to the
9
+ * tooling test directory only — NOT a blanket tests/ exclusion, because real AC tests live
10
+ * under tests/unit/{core}, tests/integration, tests/rls, ….
11
+ */
12
+ const TOOLING_TEST_DIRS = ['tests/unit/specs'];
13
+ /**
14
+ * Parse `@verifies <SPEC> <AC/US ids…>` backlinks from one file's text.
15
+ * Matches scripts/specs/verify-acs.mjs semantics: a spec id followed by a run of AC-/US- ids.
16
+ * The spec-id group is non-greedy so compound ids (e.g. PF-96-EN-01) are captured whole
17
+ * while the trailing AC/US run is still attributed correctly.
18
+ */
19
+ export function parseVerifies(fileText) {
20
+ const out = new Map();
21
+ const re = /@verifies\s+([A-Z][A-Z0-9]*(?:-[0-9A-Za-z]+)+?)\s+((?:(?:AC|US|FR)-\d+[,\s]*)+)/g;
22
+ let m;
23
+ while ((m = re.exec(fileText)) !== null) {
24
+ const specId = m[1];
25
+ const idRun = m[2];
26
+ if (!specId || !idRun)
27
+ continue;
28
+ const ids = idRun.match(/(?:AC|US|FR)-\d+/g) ?? [];
29
+ if (ids.length === 0)
30
+ continue;
31
+ const set = out.get(specId) ?? new Set();
32
+ for (const id of ids)
33
+ set.add(id);
34
+ out.set(specId, set);
35
+ }
36
+ return out;
37
+ }
38
+ /** Walk the repo's test trees and build specId → Set(verified AC/US ids). */
39
+ export function scanVerifies(repoRoot, roots = ['tests', 'src']) {
40
+ const merged = new Map();
41
+ const excludedDirs = new Set(TOOLING_TEST_DIRS.map((d) => join(repoRoot, d)));
42
+ const visit = (dir) => {
43
+ if (excludedDirs.has(dir))
44
+ return;
45
+ let entries;
46
+ try {
47
+ entries = readdirSync(dir, { withFileTypes: true });
48
+ }
49
+ catch {
50
+ return;
51
+ }
52
+ for (const e of entries) {
53
+ const name = e.name;
54
+ if (e.isDirectory()) {
55
+ if (!SKIP_DIRS.has(name))
56
+ visit(join(dir, name));
57
+ }
58
+ else if (TEST_FILE.test(name)) {
59
+ const fileMap = parseVerifies(readFileSync(join(dir, name), 'utf8'));
60
+ for (const [spec, ids] of fileMap) {
61
+ const set = merged.get(spec) ?? new Set();
62
+ for (const id of ids)
63
+ set.add(id);
64
+ merged.set(spec, set);
65
+ }
66
+ }
67
+ }
68
+ };
69
+ for (const root of roots)
70
+ visit(join(repoRoot, root));
71
+ return merged;
72
+ }
73
+ //# sourceMappingURL=verifies-scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verifies-scan.js","sourceRoot":"","sources":["../../src/lib/verifies-scan.ts"],"names":[],"mappings":"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"}