@jjchill/probity-rules 0.1.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 (71) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/GLOSSARY.template.md +33 -0
  3. package/README.md +96 -0
  4. package/dist/index.d.ts +20 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +20 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/presets/js.d.ts +63 -0
  9. package/dist/presets/js.d.ts.map +1 -0
  10. package/dist/presets/js.js +112 -0
  11. package/dist/presets/js.js.map +1 -0
  12. package/dist/presets/kmp.d.ts +27 -0
  13. package/dist/presets/kmp.d.ts.map +1 -0
  14. package/dist/presets/kmp.js +253 -0
  15. package/dist/presets/kmp.js.map +1 -0
  16. package/dist/presets/kotlin.d.ts +40 -0
  17. package/dist/presets/kotlin.d.ts.map +1 -0
  18. package/dist/presets/kotlin.js +172 -0
  19. package/dist/presets/kotlin.js.map +1 -0
  20. package/dist/presets/swift.d.ts +10 -0
  21. package/dist/presets/swift.d.ts.map +1 -0
  22. package/dist/presets/swift.js +285 -0
  23. package/dist/presets/swift.js.map +1 -0
  24. package/dist/rules/acceptance-language.d.ts +95 -0
  25. package/dist/rules/acceptance-language.d.ts.map +1 -0
  26. package/dist/rules/acceptance-language.js +443 -0
  27. package/dist/rules/acceptance-language.js.map +1 -0
  28. package/dist/rules/gates.d.ts +125 -0
  29. package/dist/rules/gates.d.ts.map +1 -0
  30. package/dist/rules/gates.js +285 -0
  31. package/dist/rules/gates.js.map +1 -0
  32. package/dist/rules/kotlin.d.ts +323 -0
  33. package/dist/rules/kotlin.d.ts.map +1 -0
  34. package/dist/rules/kotlin.js +722 -0
  35. package/dist/rules/kotlin.js.map +1 -0
  36. package/dist/rules/ports-and-adapters.d.ts +86 -0
  37. package/dist/rules/ports-and-adapters.d.ts.map +1 -0
  38. package/dist/rules/ports-and-adapters.js +366 -0
  39. package/dist/rules/ports-and-adapters.js.map +1 -0
  40. package/dist/rules/scoping.d.ts +68 -0
  41. package/dist/rules/scoping.d.ts.map +1 -0
  42. package/dist/rules/scoping.js +93 -0
  43. package/dist/rules/scoping.js.map +1 -0
  44. package/dist/rules/spec-test-parity.d.ts +164 -0
  45. package/dist/rules/spec-test-parity.d.ts.map +1 -0
  46. package/dist/rules/spec-test-parity.js +456 -0
  47. package/dist/rules/spec-test-parity.js.map +1 -0
  48. package/dist/rules/swift.d.ts +50 -0
  49. package/dist/rules/swift.d.ts.map +1 -0
  50. package/dist/rules/swift.js +50 -0
  51. package/dist/rules/swift.js.map +1 -0
  52. package/dist/rules/ubiquitous-language.d.ts +36 -0
  53. package/dist/rules/ubiquitous-language.d.ts.map +1 -0
  54. package/dist/rules/ubiquitous-language.js +140 -0
  55. package/dist/rules/ubiquitous-language.js.map +1 -0
  56. package/dist/scripts/scope-report.d.ts +3 -0
  57. package/dist/scripts/scope-report.d.ts.map +1 -0
  58. package/dist/scripts/scope-report.js +184 -0
  59. package/dist/scripts/scope-report.js.map +1 -0
  60. package/kiro/README.md +20 -0
  61. package/kiro/kiro-agent.template.json +45 -0
  62. package/kiro/kiro-transcript-to-claude.py +183 -0
  63. package/kiro/probity-kiro-translate.py +132 -0
  64. package/kiro/probity-kiro.sh +87 -0
  65. package/kiro/skill-activation-forced-eval.sh +45 -0
  66. package/package.json +68 -0
  67. package/probity.config.kmp.ts +40 -0
  68. package/probity.config.kotlin.ts +37 -0
  69. package/probity.config.swift.ts +38 -0
  70. package/probity.config.ts +43 -0
  71. package/scripts/spec-parity.mjs +345 -0
@@ -0,0 +1,456 @@
1
+ import { readdirSync, readFileSync, existsSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
3
+ /**
4
+ * Spec↔test traceability for the `acceptance-testing` workflow:
5
+ * every `## Scenario:` in a `*.feature.md` spec is claimed by an
6
+ * acceptance test, and every claim resolves to a real scenario.
7
+ *
8
+ * The link convention: an acceptance test declares the scenario it
9
+ * covers with a tag anywhere in the file (comment or annotation
10
+ * argument), one per scenario:
11
+ *
12
+ * // Covers: messaging.feature.md :: Scenario: Message is delivered
13
+ *
14
+ * Scenario keys are `<spec basename> :: <title>`, so titles are link
15
+ * keys: renaming a scenario must update its tests' tags — which
16
+ * `surfaceScenarioLinkBreakage` turns into a guided step instead of
17
+ * silent rot.
18
+ *
19
+ * In-progress scenarios being driven outside-in are exempted with
20
+ * `## Scenario (wip):` — the parity gate ignores them until the
21
+ * marker is dropped.
22
+ *
23
+ * Scenarios may additionally declare **driver scopes** — coverage
24
+ * floors naming which driver suites must cover them, on top of the
25
+ * base "at least one test" requirement:
26
+ *
27
+ * ## Scenario [system]: Payment is retried after a network failure
28
+ *
29
+ * Scope names are policy labels ("also proven against the deployed
30
+ * system"), not mechanisms; the mapping from scope name to test files
31
+ * is the `driverScopes` option. Tags never change the scenario key,
32
+ * so Covers tags and baselines are unaffected by adding one.
33
+ */
34
+ // Two spec formats during the .feature.md -> .feature migration:
35
+ // Markdown: ## Scenario (wip) [system]: Title
36
+ // Gherkin: @wip @system\n Scenario: Title
37
+ // parseScenarios below handles both, line by line.
38
+ const MD_SCENARIO = /^##\s*Scenario(\s*\((?:wip|planned)\))?(\s*\[([^\]]+)\])?\s*:\s*(.+?)\s*$/;
39
+ const GHERKIN_SCENARIO = /^Scenario(?:\s+Outline)?:\s*(.+?)\s*$/;
40
+ const COVERS_TAG = /Covers:\s*([\w.-]+\.feature(?:\.md)?)\s*::\s*Scenario:\s*([^\n]+)/g;
41
+ const SKIP_DIRS = new Set([
42
+ 'node_modules',
43
+ '.git',
44
+ 'build',
45
+ '.gradle',
46
+ 'out',
47
+ 'dist',
48
+ '.idea',
49
+ ]);
50
+ function normalizeTitle(title) {
51
+ return title
52
+ .replace(/\s*(?:\*\/|["')\]]+)?\s*$/, '')
53
+ .replace(/\s+/g, ' ')
54
+ .trim()
55
+ .toLowerCase();
56
+ }
57
+ // A spec is identified by its stem (filename without .feature/.feature.md)
58
+ // so a scenario keeps one key across the extension change and Covers tags
59
+ // resolve regardless of which extension either side currently uses.
60
+ function specStem(name) {
61
+ return basename(name).replace(/\.feature(?:\.md)?$/i, '');
62
+ }
63
+ function scenarioKey(specFile, title) {
64
+ return `${specStem(specFile)} :: ${normalizeTitle(title)}`;
65
+ }
66
+ function parseScopes(tag) {
67
+ if (!tag)
68
+ return [];
69
+ return tag
70
+ .split(',')
71
+ .map((scope) => scope.trim().toLowerCase())
72
+ .filter(Boolean);
73
+ }
74
+ export function extractScenarios(specFile, content) {
75
+ const scenarios = [];
76
+ let tags = [];
77
+ for (const line of content.split(/\r?\n/)) {
78
+ const md = line.match(MD_SCENARIO);
79
+ if (md) {
80
+ const title = md[4] ?? '';
81
+ scenarios.push({
82
+ key: scenarioKey(specFile, title),
83
+ specFile,
84
+ title: title.trim(),
85
+ wip: md[1] !== undefined,
86
+ scopes: parseScopes(md[3]),
87
+ });
88
+ tags = [];
89
+ continue;
90
+ }
91
+ const trimmed = line.trim();
92
+ if (/^@\S/.test(trimmed)) {
93
+ tags.push(...trimmed
94
+ .split(/\s+/)
95
+ .filter((token) => token.startsWith('@'))
96
+ .map((token) => token.slice(1).toLowerCase()));
97
+ continue;
98
+ }
99
+ const gherkin = trimmed.match(GHERKIN_SCENARIO);
100
+ if (gherkin) {
101
+ const title = gherkin[1] ?? '';
102
+ scenarios.push({
103
+ key: scenarioKey(specFile, title),
104
+ specFile,
105
+ title: title.trim(),
106
+ wip: tags.includes('wip') || tags.includes('planned'),
107
+ scopes: tags.filter((tag) => tag !== 'wip' && tag !== 'planned'),
108
+ });
109
+ tags = [];
110
+ continue;
111
+ }
112
+ // Tags only attach to the next Scenario; any non-tag, non-comment
113
+ // content (a Feature line, a step) ends a dangling tag block.
114
+ if (trimmed !== '' && !trimmed.startsWith('#'))
115
+ tags = [];
116
+ }
117
+ return scenarios;
118
+ }
119
+ export function extractCoversRefs(testFile, content) {
120
+ const refs = [];
121
+ for (const match of content.matchAll(COVERS_TAG)) {
122
+ const specFile = match[1] ?? '';
123
+ const title = match[2] ?? '';
124
+ refs.push({
125
+ key: scenarioKey(specFile, title),
126
+ testFile,
127
+ specFile,
128
+ title: title.replace(/\s*(?:\*\/|["')\]]+)?\s*$/, '').trim(),
129
+ });
130
+ }
131
+ return refs;
132
+ }
133
+ function walk(dir, out = []) {
134
+ let entries;
135
+ try {
136
+ entries = readdirSync(dir, { withFileTypes: true });
137
+ }
138
+ catch {
139
+ return out;
140
+ }
141
+ for (const entry of entries) {
142
+ if (entry.isDirectory()) {
143
+ if (!SKIP_DIRS.has(entry.name))
144
+ walk(join(dir, entry.name), out);
145
+ }
146
+ else {
147
+ out.push(join(dir, entry.name));
148
+ }
149
+ }
150
+ return out;
151
+ }
152
+ const DEFAULT_TEST_FILE_PATTERN = /[/\\]acceptance[/\\]/;
153
+ /** Parses a baseline file into normalized scenario keys. Missing file → empty set. */
154
+ export function readBaseline(path) {
155
+ if (!path || !existsSync(path))
156
+ return new Set();
157
+ return new Set(readFileSync(path, 'utf8')
158
+ .split('\n')
159
+ .map((line) => line.trim())
160
+ .filter((line) => line && !line.startsWith('#'))
161
+ .flatMap((line) => {
162
+ const idx = line.indexOf(' :: ');
163
+ if (idx === -1)
164
+ return [];
165
+ const spec = line.slice(0, idx).trim();
166
+ const title = line.slice(idx + 4);
167
+ return [scenarioKey(spec, title)];
168
+ }));
169
+ }
170
+ /** All spec files (either extension) under a directory. */
171
+ export function findSpecFiles(specsDir) {
172
+ return walk(specsDir).filter((file) => file.endsWith('.feature.md') || file.endsWith('.feature'));
173
+ }
174
+ export function scanSpecs(specsDir) {
175
+ return findSpecFiles(specsDir).flatMap((file) => extractScenarios(file, readFileSync(file, 'utf8')));
176
+ }
177
+ export function scanCoversRefs(testRoots, testFilePattern) {
178
+ return testRoots
179
+ .flatMap((root) => walk(root))
180
+ .filter((file) => testFilePattern.test(file) &&
181
+ !file.endsWith('.feature.md') &&
182
+ !file.endsWith('.feature'))
183
+ .flatMap((file) => extractCoversRefs(file, readFileSync(file, 'utf8')));
184
+ }
185
+ function formatList(lines, max = 10) {
186
+ const shown = lines.slice(0, max).map((line) => ` ${line}`);
187
+ if (lines.length > max)
188
+ shown.push(` …and ${lines.length - max} more`);
189
+ return shown.join('\n');
190
+ }
191
+ /**
192
+ * Bidirectional parity gate on `git commit`: blocks when any
193
+ * non-wip `## Scenario:` heading in `specsDir` has no acceptance
194
+ * test claiming it via a `Covers:` tag, or when any `Covers:` tag
195
+ * points at a scenario that no longer exists (spec renamed or
196
+ * removed). Deterministic filesystem scan — no AI call; milliseconds
197
+ * at typical spec-suite scale.
198
+ *
199
+ * The test claiming a scenario may still be red or quarantined —
200
+ * parity requires the *link*, not a passing run; keeping in-progress
201
+ * specs out of the pipeline is the pipeline's job. Alternatively
202
+ * mark the scenario `## Scenario (wip):` to exempt it entirely.
203
+ *
204
+ * Applies to: command actions matching `git commit`.
205
+ *
206
+ * @param options.specsDir — absolute path to the specs directory
207
+ * (e.g. `<root>/docs/specs`). A missing directory passes: no
208
+ * specs, nothing to enforce.
209
+ * @param options.testRoots — absolute paths to scan for acceptance
210
+ * tests (the repo root is fine; node_modules/build dirs are
211
+ * skipped).
212
+ * @param options.testFilePattern — which files count as acceptance
213
+ * tests (default: any file under an `acceptance/` directory).
214
+ * @param options.baselinePath — optional incremental-adoption
215
+ * baseline; scenarios listed there are exempt from the orphan
216
+ * check (see {@link readBaseline}). Missing file → full enforcement.
217
+ * @param options.driverScopes — optional per-scenario driver mapping;
218
+ * see {@link DriverScope}. Scenarios tagged `[scope]` (and every
219
+ * scenario, for `defaultScopes` entries) must be covered by a test
220
+ * matching that scope's `filePattern`, on top of the base check.
221
+ * @param options.defaultScopes — scope names required of untagged
222
+ * scenarios too (the project's standard driver set).
223
+ */
224
+ export function enforceSpecTestParity(options) {
225
+ const pattern = options.testFilePattern ?? DEFAULT_TEST_FILE_PATTERN;
226
+ const scopes = new Map((options.driverScopes ?? []).map((scope) => [scope.name.toLowerCase(), scope]));
227
+ const defaultScopes = (options.defaultScopes ?? []).map((name) => name.toLowerCase());
228
+ return function enforceSpecTestParity(action) {
229
+ if (action.kind !== 'command')
230
+ return { kind: 'pass' };
231
+ if (!/git commit/.test(action.command))
232
+ return { kind: 'pass' };
233
+ if (!existsSync(options.specsDir))
234
+ return { kind: 'pass' };
235
+ // During the .feature.md -> .feature migration a spec is one extension
236
+ // or the other; the same stem in both forms double-counts scenarios and
237
+ // masks a half-finished rename.
238
+ const specFiles = findSpecFiles(options.specsDir);
239
+ const byStem = new Map();
240
+ for (const file of specFiles) {
241
+ const stem = specStem(file);
242
+ const existing = byStem.get(stem);
243
+ if (existing) {
244
+ return {
245
+ kind: 'violation',
246
+ reason: `Spec "${stem}" exists in both .feature and .feature.md forms:\n` +
247
+ ` ${existing}\n ${file}\n` +
248
+ 'Finish the migration for this spec — keep a single extension.',
249
+ };
250
+ }
251
+ byStem.set(stem, file);
252
+ }
253
+ const scenarios = specFiles.flatMap((file) => extractScenarios(file, readFileSync(file, 'utf8')));
254
+ const refs = scanCoversRefs(options.testRoots, pattern);
255
+ const baseline = readBaseline(options.baselinePath);
256
+ const claimed = new Set(refs.map((ref) => ref.key));
257
+ const known = new Set(scenarios.map((scenario) => scenario.key));
258
+ const orphaned = scenarios.filter((s) => !s.wip && !claimed.has(s.key) && !baseline.has(s.key));
259
+ const dangling = refs.filter((ref) => !known.has(ref.key));
260
+ const unknownScopes = [];
261
+ const missingScopes = [];
262
+ for (const scenario of scenarios) {
263
+ if (scenario.wip || baseline.has(scenario.key))
264
+ continue;
265
+ const required = [...new Set([...defaultScopes, ...scenario.scopes])];
266
+ for (const name of required) {
267
+ const scope = scopes.get(name);
268
+ if (!scope) {
269
+ unknownScopes.push(`${basename(scenario.specFile)} :: ${scenario.title} — [${name}]`);
270
+ continue;
271
+ }
272
+ const covered = refs.some((ref) => ref.key === scenario.key && scope.filePattern.test(ref.testFile));
273
+ if (!covered) {
274
+ missingScopes.push(`${basename(scenario.specFile)} :: ${scenario.title} — needs [${name}] coverage`);
275
+ }
276
+ }
277
+ }
278
+ if (orphaned.length === 0 &&
279
+ dangling.length === 0 &&
280
+ unknownScopes.length === 0 &&
281
+ missingScopes.length === 0)
282
+ return { kind: 'pass' };
283
+ const sections = ['Spec↔test parity check failed.'];
284
+ if (orphaned.length > 0) {
285
+ sections.push('Scenarios with no covering acceptance test (add a test with ' +
286
+ '`// Covers: <spec>.feature.md :: Scenario: <title>`, or mark ' +
287
+ 'the heading `## Scenario (wip):` while driving it):\n' +
288
+ formatList(orphaned.map((s) => `${basename(s.specFile)} :: ${s.title}`)));
289
+ }
290
+ if (dangling.length > 0) {
291
+ sections.push('Covers tags pointing at no existing scenario (spec renamed or ' +
292
+ 'removed — update or delete these tests):\n' +
293
+ formatList(dangling.map((r) => `${r.testFile} → ${r.specFile} :: ${r.title}`)));
294
+ }
295
+ if (missingScopes.length > 0) {
296
+ sections.push('Scenarios covered, but not by every driver scope they require ' +
297
+ '(add a covering test in the named suite — with shared ' +
298
+ 'scenario bodies this is a thin spec class calling the ' +
299
+ 'existing body):\n' +
300
+ formatList(missingScopes));
301
+ }
302
+ if (unknownScopes.length > 0) {
303
+ sections.push('Scenario tags naming driver scopes this gate does not know ' +
304
+ `(known scopes: ${scopes.size > 0 ? [...scopes.keys()].join(', ') : 'none configured'} — ` +
305
+ 'fix the tag or add the scope to `driverScopes`):\n' +
306
+ formatList(unknownScopes));
307
+ }
308
+ return { kind: 'violation', reason: sections.join('\n') };
309
+ };
310
+ }
311
+ /**
312
+ * Matches a test-case declaration in the acceptance layer: a Kotlin/
313
+ * Java `@Test` annotation or a Swift XCTest `func test…` method. The
314
+ * pattern is intentionally coarse — it only needs to detect that a
315
+ * write ADDS test cases, not parse them.
316
+ */
317
+ const DEFAULT_TEST_DECLARATION = /@Test\b|\bfunc\s+test\w*\s*\(/g;
318
+ function countTests(content, pattern) {
319
+ return [...content.matchAll(pattern)].length;
320
+ }
321
+ /**
322
+ * Spec-first, made mechanical: a write that adds a new acceptance
323
+ * test case is blocked unless it also adds a `Covers:` tag pointing
324
+ * at a `## Scenario:` heading that ALREADY EXISTS in `specsDir`. The
325
+ * executable specification (`*.feature.md`) must be written before
326
+ * the test that claims it — an agent that codes the test first gets
327
+ * told exactly what to create and where.
328
+ *
329
+ * This is the write-time front half of the traceability story;
330
+ * {@link enforceSpecTestParity} is the commit-time back half (every
331
+ * scenario covered, every tag resolving). Together they close the
332
+ * gap where an agent authors a whole acceptance suite with no
333
+ * feature file and nothing objects until commit — or ever, if the
334
+ * tests carry no tags at all.
335
+ *
336
+ * Delta-based and deterministic (no AI call): only NEW test cases
337
+ * demand a NEW resolving tag, so edits inside an existing brownfield
338
+ * test file pass untouched, and a pre-existing dangling tag can't
339
+ * block unrelated work (the commit gate owns that). `(wip)` and
340
+ * `(planned)` scenarios count as existing — they are exactly the
341
+ * headings outside-in work drives against.
342
+ *
343
+ * Applies to: write actions — scope it via a `{ files, rules }`
344
+ * block to the acceptance test-case layer only (e.g.
345
+ * `**\/acceptance/**\/*Spec.kt`), never to drivers/DSL/infrastructure.
346
+ *
347
+ * @param options.specsDir — absolute path to the specs directory.
348
+ * Missing directory ⇒ every new tag is unresolved ⇒ the deny
349
+ * message says to create the first feature file (greenfield
350
+ * enforcement, not a free pass).
351
+ * @param options.testDeclarationPattern — what counts as a test-case
352
+ * declaration (default: Kotlin/Java `@Test` or Swift `func test…`).
353
+ */
354
+ export function requireSpecBackedAcceptanceTest(options) {
355
+ const declaration = options.testDeclarationPattern ?? DEFAULT_TEST_DECLARATION;
356
+ return async function requireSpecBackedAcceptanceTest(action, ctx) {
357
+ if (action.kind !== 'write')
358
+ return { kind: 'pass' };
359
+ const before = await ctx?.readFile?.(action.path);
360
+ const beforeContent = before?.kind === 'present' ? before.content : '';
361
+ const testsAdded = countTests(action.content, declaration) > countTests(beforeContent, declaration);
362
+ if (!testsAdded)
363
+ return { kind: 'pass' };
364
+ const beforeKeys = new Set(extractCoversRefs(action.path, beforeContent).map((ref) => ref.key));
365
+ const afterRefs = extractCoversRefs(action.path, action.content);
366
+ const newRefs = afterRefs.filter((ref) => !beforeKeys.has(ref.key));
367
+ const known = new Set(scanSpecs(options.specsDir).map((s) => s.key));
368
+ if (newRefs.length === 0) {
369
+ // No NEW tag — still fine when the file already claims a real
370
+ // scenario: "one scenario, many drivers" adds a second/third
371
+ // test transcribing an already-covered scenario to a file whose
372
+ // tag resolves. Only a file with no resolving tag at all is a
373
+ // spec-first violation. (File-level granularity by design: the
374
+ // commit-time parity gate owns the per-scenario bookkeeping.)
375
+ if (afterRefs.some((ref) => known.has(ref.key)))
376
+ return { kind: 'pass' };
377
+ return {
378
+ kind: 'violation',
379
+ reason: 'This write adds a new acceptance test case with no ' +
380
+ '`Covers:` tag resolving to an existing scenario. ' +
381
+ 'Acceptance tests are written spec-first: ' +
382
+ `add a \`## Scenario: <title>\` to a *.feature.md under ` +
383
+ `${options.specsDir} describing the behavior in domain ` +
384
+ 'language, then re-apply this test with\n' +
385
+ ' // Covers: <spec>.feature.md :: Scenario: <title>\n' +
386
+ 'If the scenario is still being shaped, write it as ' +
387
+ '`## Scenario (wip):` — it still counts.',
388
+ };
389
+ }
390
+ const unresolved = newRefs.filter((ref) => !known.has(ref.key));
391
+ if (unresolved.length === 0)
392
+ return { kind: 'pass' };
393
+ return {
394
+ kind: 'violation',
395
+ reason: 'This write adds acceptance test(s) whose `Covers:` tag points ' +
396
+ 'at a scenario that does not exist yet. The feature file comes ' +
397
+ 'first: create the scenario heading (exact title match, ' +
398
+ `case-insensitive) under ${options.specsDir}, then re-apply ` +
399
+ 'this test.\n' +
400
+ formatList(unresolved.map((ref) => `${ref.specFile} :: Scenario: ${ref.title}`)),
401
+ };
402
+ };
403
+ }
404
+ /**
405
+ * Surfaces link breakage at the moment it is created: when a write
406
+ * to a `*.feature.md` file removes or renames a `## Scenario:`
407
+ * heading that acceptance tests still claim, the write is blocked
408
+ * with the list of affected tests — update their `Covers:` tags (or
409
+ * delete the tests) in the same change, then re-apply the spec edit.
410
+ * Removing a scenario nothing covers passes silently.
411
+ *
412
+ * Applies to: write actions to `*.feature.md` files (scope via a
413
+ * `{ files, rules }` block). Deterministic — no AI call.
414
+ *
415
+ * @param options.testRoots — absolute paths to scan for acceptance
416
+ * tests.
417
+ * @param options.testFilePattern — which files count as acceptance
418
+ * tests (default: any file under an `acceptance/` directory).
419
+ */
420
+ export function surfaceScenarioLinkBreakage(options) {
421
+ const pattern = options.testFilePattern ?? DEFAULT_TEST_FILE_PATTERN;
422
+ return async function surfaceScenarioLinkBreakage(action, ctx) {
423
+ if (action.kind !== 'write')
424
+ return { kind: 'pass' };
425
+ if (!action.path.endsWith('.feature.md') && !action.path.endsWith('.feature'))
426
+ return { kind: 'pass' };
427
+ const before = await ctx?.readFile?.(action.path);
428
+ if (!before || before.kind !== 'present')
429
+ return { kind: 'pass' };
430
+ const beforeKeys = new Map(extractScenarios(action.path, before.content).map((s) => [s.key, s]));
431
+ const afterKeys = new Set(extractScenarios(action.path, action.content).map((s) => s.key));
432
+ const removed = [...beforeKeys.values()].filter((s) => !afterKeys.has(s.key));
433
+ if (removed.length === 0)
434
+ return { kind: 'pass' };
435
+ const refs = scanCoversRefs(options.testRoots, pattern);
436
+ const broken = removed.flatMap((scenario) => {
437
+ const claimants = [
438
+ ...new Set(refs.filter((ref) => ref.key === scenario.key).map((ref) => ref.testFile)),
439
+ ];
440
+ return claimants.length > 0
441
+ ? [`"${scenario.title}" is still covered by:\n` + formatList(claimants)]
442
+ : [];
443
+ });
444
+ if (broken.length === 0)
445
+ return { kind: 'pass' };
446
+ return {
447
+ kind: 'violation',
448
+ reason: 'This edit removes or renames scenario(s) that acceptance ' +
449
+ 'tests still claim. Update those tests’ Covers: tags (or ' +
450
+ 'delete the tests) in the same change, then re-apply this ' +
451
+ 'spec edit.\n' +
452
+ broken.join('\n'),
453
+ };
454
+ };
455
+ }
456
+ //# sourceMappingURL=spec-test-parity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spec-test-parity.js","sourceRoot":"","sources":["../../rules/spec-test-parity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAC/D,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAI1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,iEAAiE;AACjE,iDAAiD;AACjD,+CAA+C;AAC/C,mDAAmD;AACnD,MAAM,WAAW,GACf,2EAA2E,CAAA;AAC7E,MAAM,gBAAgB,GAAG,uCAAuC,CAAA;AAChE,MAAM,UAAU,GAAG,oEAAoE,CAAA;AACvF,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;IACxB,cAAc;IACd,MAAM;IACN,OAAO;IACP,SAAS;IACT,KAAK;IACL,MAAM;IACN,OAAO;CACR,CAAC,CAAA;AAEF,SAAS,cAAc,CAAC,KAAa;IACnC,OAAO,KAAK;SACT,OAAO,CAAC,2BAA2B,EAAE,EAAE,CAAC;SACxC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE;SACN,WAAW,EAAE,CAAA;AAClB,CAAC;AAED,2EAA2E;AAC3E,0EAA0E;AAC1E,oEAAoE;AACpE,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,sBAAsB,EAAE,EAAE,CAAC,CAAA;AAC3D,CAAC;AAED,SAAS,WAAW,CAAC,QAAgB,EAAE,KAAa;IAClD,OAAO,GAAG,QAAQ,CAAC,QAAQ,CAAC,OAAO,cAAc,CAAC,KAAK,CAAC,EAAE,CAAA;AAC5D,CAAC;AAYD,SAAS,WAAW,CAAC,GAAuB;IAC1C,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAA;IACnB,OAAO,GAAG;SACP,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;SAC1C,MAAM,CAAC,OAAO,CAAC,CAAA;AACpB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,QAAgB,EAAE,OAAe;IAChE,MAAM,SAAS,GAAe,EAAE,CAAA;IAChC,IAAI,IAAI,GAAa,EAAE,CAAA;IACvB,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QAC1C,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;QAClC,IAAI,EAAE,EAAE,CAAC;YACP,MAAM,KAAK,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;YACzB,SAAS,CAAC,IAAI,CAAC;gBACb,GAAG,EAAE,WAAW,CAAC,QAAQ,EAAE,KAAK,CAAC;gBACjC,QAAQ;gBACR,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE;gBACnB,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,SAAS;gBACxB,MAAM,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;aAC3B,CAAC,CAAA;YACF,IAAI,GAAG,EAAE,CAAA;YACT,SAAQ;QACV,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAA;QAC3B,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACzB,IAAI,CAAC,IAAI,CACP,GAAG,OAAO;iBACP,KAAK,CAAC,KAAK,CAAC;iBACZ,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;iBACxC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAChD,CAAA;YACD,SAAQ;QACV,CAAC;QACD,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAA;QAC/C,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;YAC9B,SAAS,CAAC,IAAI,CAAC;gBACb,GAAG,EAAE,WAAW,CAAC,QAAQ,EAAE,KAAK,CAAC;gBACjC,QAAQ;gBACR,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE;gBACnB,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;gBACrD,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,KAAK,IAAI,GAAG,KAAK,SAAS,CAAC;aACjE,CAAC,CAAA;YACF,IAAI,GAAG,EAAE,CAAA;YACT,SAAQ;QACV,CAAC;QACD,kEAAkE;QAClE,8DAA8D;QAC9D,IAAI,OAAO,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,IAAI,GAAG,EAAE,CAAA;IAC3D,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,QAAgB,EAAE,OAAe;IACjE,MAAM,IAAI,GAAgB,EAAE,CAAA;IAC5B,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QACjD,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QAC/B,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QAC5B,IAAI,CAAC,IAAI,CAAC;YACR,GAAG,EAAE,WAAW,CAAC,QAAQ,EAAE,KAAK,CAAC;YACjC,QAAQ;YACR,QAAQ;YACR,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,2BAA2B,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE;SAC7D,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAS,IAAI,CAAC,GAAW,EAAE,MAAgB,EAAE;IAC3C,IAAI,OAAO,CAAA;IACX,IAAI,CAAC;QACH,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAA;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,CAAA;IACZ,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACxB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;gBAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAA;QAClE,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;QACjC,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,MAAM,yBAAyB,GAAG,sBAAsB,CAAA;AAoDxD,sFAAsF;AACtF,MAAM,UAAU,YAAY,CAAC,IAAwB;IACnD,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,GAAG,EAAE,CAAA;IAChD,OAAO,IAAI,GAAG,CACZ,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC;SACvB,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;SAC1B,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;SAC/C,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QAChB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QAChC,IAAI,GAAG,KAAK,CAAC,CAAC;YAAE,OAAO,EAAE,CAAA;QACzB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAA;QACtC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAA;QACjC,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAA;IACnC,CAAC,CAAC,CACL,CAAA;AACH,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC5C,OAAO,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,CAC1B,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CACpE,CAAA;AACH,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,QAAgB;IACxC,OAAO,aAAa,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAC9C,gBAAgB,CAAC,IAAI,EAAE,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CACnD,CAAA;AACH,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,SAAmB,EACnB,eAAuB;IAEvB,OAAO,SAAS;SACb,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;SAC7B,MAAM,CACL,CAAC,IAAI,EAAE,EAAE,CACP,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC;QAC1B,CAAC,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC;QAC7B,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAC7B;SACA,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,iBAAiB,CAAC,IAAI,EAAE,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;AAC3E,CAAC;AAED,SAAS,UAAU,CAAC,KAAe,EAAE,GAAG,GAAG,EAAE;IAC3C,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,EAAE,CAAC,CAAA;IAC9D,IAAI,KAAK,CAAC,MAAM,GAAG,GAAG;QAAE,KAAK,CAAC,IAAI,CAAC,YAAY,KAAK,CAAC,MAAM,GAAG,GAAG,OAAO,CAAC,CAAA;IACzE,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAoB;IACxD,MAAM,OAAO,GAAG,OAAO,CAAC,eAAe,IAAI,yBAAyB,CAAA;IACpE,MAAM,MAAM,GAAG,IAAI,GAAG,CACpB,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,KAAK,CAAC,CAAC,CAC/E,CAAA;IACD,MAAM,aAAa,GAAG,CAAC,OAAO,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAA;IACrF,OAAO,SAAS,qBAAqB,CAAC,MAAc;QAClD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;QACtD,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;QAC/D,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,QAAQ,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;QAC1D,uEAAuE;QACvE,wEAAwE;QACxE,gCAAgC;QAChC,MAAM,SAAS,GAAG,aAAa,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;QACjD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAA;QACxC,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;YAC7B,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAA;YAC3B,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;YACjC,IAAI,QAAQ,EAAE,CAAC;gBACb,OAAO;oBACL,IAAI,EAAE,WAAW;oBACjB,MAAM,EACJ,SAAS,IAAI,oDAAoD;wBACjE,OAAO,QAAQ,SAAS,IAAI,IAAI;wBAChC,+DAA+D;iBAClE,CAAA;YACH,CAAC;YACD,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;QACxB,CAAC;QACD,MAAM,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAC3C,gBAAgB,CAAC,IAAI,EAAE,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CACnD,CAAA;QACD,MAAM,IAAI,GAAG,cAAc,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;QACvD,MAAM,QAAQ,GAAG,YAAY,CAAC,OAAO,CAAC,YAAY,CAAC,CAAA;QACnD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAA;QACnD,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAA;QAChE,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,CAC/B,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAC7D,CAAA;QACD,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAA;QAC1D,MAAM,aAAa,GAAa,EAAE,CAAA;QAClC,MAAM,aAAa,GAAa,EAAE,CAAA;QAClC,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;YACjC,IAAI,QAAQ,CAAC,GAAG,IAAI,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC;gBAAE,SAAQ;YACxD,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,aAAa,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;YACrE,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;gBAC5B,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;gBAC9B,IAAI,CAAC,KAAK,EAAE,CAAC;oBACX,aAAa,CAAC,IAAI,CAChB,GAAG,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,QAAQ,CAAC,KAAK,OAAO,IAAI,GAAG,CAClE,CAAA;oBACD,SAAQ;gBACV,CAAC;gBACD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CACvB,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,QAAQ,CAAC,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAC1E,CAAA;gBACD,IAAI,CAAC,OAAO,EAAE,CAAC;oBACb,aAAa,CAAC,IAAI,CAChB,GAAG,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,QAAQ,CAAC,KAAK,aAAa,IAAI,YAAY,CACjF,CAAA;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QACD,IACE,QAAQ,CAAC,MAAM,KAAK,CAAC;YACrB,QAAQ,CAAC,MAAM,KAAK,CAAC;YACrB,aAAa,CAAC,MAAM,KAAK,CAAC;YAC1B,aAAa,CAAC,MAAM,KAAK,CAAC;YAE1B,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;QACzB,MAAM,QAAQ,GAAa,CAAC,gCAAgC,CAAC,CAAA;QAC7D,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,QAAQ,CAAC,IAAI,CACX,8DAA8D;gBAC5D,+DAA+D;gBAC/D,uDAAuD;gBACvD,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAC3E,CAAA;QACH,CAAC;QACD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,QAAQ,CAAC,IAAI,CACX,gEAAgE;gBAC9D,4CAA4C;gBAC5C,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,MAAM,CAAC,CAAC,QAAQ,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CACjF,CAAA;QACH,CAAC;QACD,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7B,QAAQ,CAAC,IAAI,CACX,gEAAgE;gBAC9D,wDAAwD;gBACxD,wDAAwD;gBACxD,mBAAmB;gBACnB,UAAU,CAAC,aAAa,CAAC,CAC5B,CAAA;QACH,CAAC;QACD,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7B,QAAQ,CAAC,IAAI,CACX,6DAA6D;gBAC3D,kBAAkB,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,iBAAiB,KAAK;gBAC1F,oDAAoD;gBACpD,UAAU,CAAC,aAAa,CAAC,CAC5B,CAAA;QACH,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAA;IAC3D,CAAC,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,wBAAwB,GAAG,gCAAgC,CAAA;AAEjE,SAAS,UAAU,CAAC,OAAe,EAAE,OAAe;IAClD,OAAO,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAA;AAC9C,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,UAAU,+BAA+B,CAAC,OAG/C;IACC,MAAM,WAAW,GAAG,OAAO,CAAC,sBAAsB,IAAI,wBAAwB,CAAA;IAC9E,OAAO,KAAK,UAAU,+BAA+B,CACnD,MAAc,EACd,GAAiB;QAEjB,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;QACpD,MAAM,MAAM,GAAG,MAAM,GAAG,EAAE,QAAQ,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QACjD,MAAM,aAAa,GAAG,MAAM,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAA;QACtE,MAAM,UAAU,GACd,UAAU,CAAC,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC,GAAG,UAAU,CAAC,aAAa,EAAE,WAAW,CAAC,CAAA;QAClF,IAAI,CAAC,UAAU;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;QACxC,MAAM,UAAU,GAAG,IAAI,GAAG,CACxB,iBAAiB,CAAC,MAAM,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CACpE,CAAA;QACD,MAAM,SAAS,GAAG,iBAAiB,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,CAAA;QAChE,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAA;QACnE,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;QACpE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,8DAA8D;YAC9D,6DAA6D;YAC7D,gEAAgE;YAChE,8DAA8D;YAC9D,+DAA+D;YAC/D,8DAA8D;YAC9D,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;YACxE,OAAO;gBACL,IAAI,EAAE,WAAW;gBACjB,MAAM,EACJ,qDAAqD;oBACrD,mDAAmD;oBACnD,2CAA2C;oBAC3C,yDAAyD;oBACzD,GAAG,OAAO,CAAC,QAAQ,qCAAqC;oBACxD,0CAA0C;oBAC1C,uDAAuD;oBACvD,qDAAqD;oBACrD,yCAAyC;aAC5C,CAAA;QACH,CAAC;QACD,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAA;QAC/D,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;QACpD,OAAO;YACL,IAAI,EAAE,WAAW;YACjB,MAAM,EACJ,gEAAgE;gBAChE,gEAAgE;gBAChE,yDAAyD;gBACzD,2BAA2B,OAAO,CAAC,QAAQ,kBAAkB;gBAC7D,cAAc;gBACd,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,QAAQ,iBAAiB,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;SACnF,CAAA;IACH,CAAC,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,2BAA2B,CACzC,OAAsC;IAEtC,MAAM,OAAO,GAAG,OAAO,CAAC,eAAe,IAAI,yBAAyB,CAAA;IACpE,OAAO,KAAK,UAAU,2BAA2B,CAC/C,MAAc,EACd,GAAiB;QAEjB,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;QACpD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC;YAC3E,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;QACzB,MAAM,MAAM,GAAG,MAAM,GAAG,EAAE,QAAQ,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QACjD,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;QACjE,MAAM,UAAU,GAAG,IAAI,GAAG,CACxB,gBAAgB,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CACrE,CAAA;QACD,MAAM,SAAS,GAAG,IAAI,GAAG,CACvB,gBAAgB,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAChE,CAAA;QACD,MAAM,OAAO,GAAG,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;QAC7E,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;QACjD,MAAM,IAAI,GAAG,cAAc,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;QACvD,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE;YAC1C,MAAM,SAAS,GAAG;gBAChB,GAAG,IAAI,GAAG,CACR,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAC1E;aACF,CAAA;YACD,OAAO,SAAS,CAAC,MAAM,GAAG,CAAC;gBACzB,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,KAAK,0BAA0B,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;gBACxE,CAAC,CAAC,EAAE,CAAA;QACR,CAAC,CAAC,CAAA;QACF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;QAChD,OAAO;YACL,IAAI,EAAE,WAAW;YACjB,MAAM,EACJ,2DAA2D;gBAC3D,0DAA0D;gBAC1D,2DAA2D;gBAC3D,cAAc;gBACd,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;SACpB,CAAA;IACH,CAAC,CAAA;AACH,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Swift/iOS-shaped deterministic rules and constants for the Probity
3
+ * presets — the Apple-toolchain counterpart of ./kotlin.ts.
4
+ *
5
+ * Most of the enforcement stack is language-neutral and reused as-is
6
+ * (spec-test-parity, acceptance-language, glossary rules, the
7
+ * mutation-probe pair, the green-run gate). This module supplies only
8
+ * what is genuinely Swift/Xcode-specific: content screens for the
9
+ * acceptance suite and the xcodebuild patterns the generic gates need.
10
+ * Calibrated against a production iOS app (CocoaPods + SwiftPM
11
+ * workspace, XCUITest + app-hosted component-test targets, MVVM view
12
+ * models over use-case ports) — adjust to your layout.
13
+ */
14
+ /**
15
+ * Fixed waits in test code — the top cause of flaky XCUITest suites.
16
+ * Matches `sleep(2)`, `usleep(...)`, `Thread.sleep(forTimeInterval:)`,
17
+ * and `try await Task.sleep(...)`. Synchronize with XCTest
18
+ * expectations, predicate expectations, or element-existence timeouts
19
+ * instead (`waitForExistence(timeout:)`).
20
+ */
21
+ export declare const SWIFT_FIXED_SLEEPS: RegExp;
22
+ /**
23
+ * XCUITest mechanics — `XCUIApplication`, `XCUIElement`, coordinate
24
+ * taps. In the four-layer model these belong ONLY in protocol drivers
25
+ * (and only in the driver that owns the deployed-app scope): scope
26
+ * this screen to the spec/scenario/DSL layers, where any match means
27
+ * UI mechanics have leaked upward.
28
+ */
29
+ export declare const XCUITEST_MECHANICS: RegExp;
30
+ /**
31
+ * An `xcodebuild … test` invocation, for the green-run commit gate.
32
+ * Matches plain `test`, `test-without-building`, and scheme-qualified
33
+ * forms. Pair with {@link XCODEBUILD_TEST_SUCCEEDED} /
34
+ * {@link XCODEBUILD_TEST_FAILED}: xcodebuild prints
35
+ * `** TEST SUCCEEDED **` / `** TEST FAILED **` verdict banners, and
36
+ * `xcrun xcresulttool get test-results summary` reports
37
+ * `"result" : "Passed"`. Either counts as evidence; a recorded
38
+ * invocation with neither is not a passing suite.
39
+ *
40
+ * `-quiet` suppresses the verdict banner (confirmed against a real
41
+ * run: the last line is just `Testing started`), so with a -quiet
42
+ * runbook the xcresulttool readback is the ONLY green evidence the
43
+ * gate will see — make it a mandatory step of "run the tests".
44
+ */
45
+ export declare const XCODEBUILD_TEST_COMMAND: RegExp;
46
+ export declare const XCODEBUILD_TEST_SUCCEEDED: RegExp;
47
+ export declare const XCODEBUILD_TEST_FAILED: RegExp;
48
+ /** Probe carriers for enforceProbeReversion on an Apple codebase. */
49
+ export declare const SWIFT_PROBE_FILE_PATTERN: RegExp;
50
+ //# sourceMappingURL=swift.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"swift.d.ts","sourceRoot":"","sources":["../../rules/swift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,QACsB,CAAA;AAErD;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,QAA+C,CAAA;AAE9E;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,uBAAuB,QACqD,CAAA;AAEzF,eAAO,MAAM,yBAAyB,QACyC,CAAA;AAE/E,eAAO,MAAM,sBAAsB,QACqD,CAAA;AAExF,qEAAqE;AACrE,eAAO,MAAM,wBAAwB,QAAsB,CAAA"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Swift/iOS-shaped deterministic rules and constants for the Probity
3
+ * presets — the Apple-toolchain counterpart of ./kotlin.ts.
4
+ *
5
+ * Most of the enforcement stack is language-neutral and reused as-is
6
+ * (spec-test-parity, acceptance-language, glossary rules, the
7
+ * mutation-probe pair, the green-run gate). This module supplies only
8
+ * what is genuinely Swift/Xcode-specific: content screens for the
9
+ * acceptance suite and the xcodebuild patterns the generic gates need.
10
+ * Calibrated against a production iOS app (CocoaPods + SwiftPM
11
+ * workspace, XCUITest + app-hosted component-test targets, MVVM view
12
+ * models over use-case ports) — adjust to your layout.
13
+ */
14
+ /**
15
+ * Fixed waits in test code — the top cause of flaky XCUITest suites.
16
+ * Matches `sleep(2)`, `usleep(...)`, `Thread.sleep(forTimeInterval:)`,
17
+ * and `try await Task.sleep(...)`. Synchronize with XCTest
18
+ * expectations, predicate expectations, or element-existence timeouts
19
+ * instead (`waitForExistence(timeout:)`).
20
+ */
21
+ export const SWIFT_FIXED_SLEEPS = /\b(?:sleep|usleep)\s*\(|Thread\.sleep|Task\.sleep/;
22
+ /**
23
+ * XCUITest mechanics — `XCUIApplication`, `XCUIElement`, coordinate
24
+ * taps. In the four-layer model these belong ONLY in protocol drivers
25
+ * (and only in the driver that owns the deployed-app scope): scope
26
+ * this screen to the spec/scenario/DSL layers, where any match means
27
+ * UI mechanics have leaked upward.
28
+ */
29
+ export const XCUITEST_MECHANICS = /XCUIApplication|XCUIElement|XCUICoordinate/;
30
+ /**
31
+ * An `xcodebuild … test` invocation, for the green-run commit gate.
32
+ * Matches plain `test`, `test-without-building`, and scheme-qualified
33
+ * forms. Pair with {@link XCODEBUILD_TEST_SUCCEEDED} /
34
+ * {@link XCODEBUILD_TEST_FAILED}: xcodebuild prints
35
+ * `** TEST SUCCEEDED **` / `** TEST FAILED **` verdict banners, and
36
+ * `xcrun xcresulttool get test-results summary` reports
37
+ * `"result" : "Passed"`. Either counts as evidence; a recorded
38
+ * invocation with neither is not a passing suite.
39
+ *
40
+ * `-quiet` suppresses the verdict banner (confirmed against a real
41
+ * run: the last line is just `Testing started`), so with a -quiet
42
+ * runbook the xcresulttool readback is the ONLY green evidence the
43
+ * gate will see — make it a mandatory step of "run the tests".
44
+ */
45
+ export const XCODEBUILD_TEST_COMMAND = /\bxcodebuild\b[\s\S]*\btest(?:-without-building)?\b|xcresulttool\s+get\s+test-results/;
46
+ export const XCODEBUILD_TEST_SUCCEEDED = /\*\*\s*TEST SUCCEEDED\s*\*\*|"result"\s*:\s*"Passed"|Test Suite '.*' passed/;
47
+ export const XCODEBUILD_TEST_FAILED = /\*\*\s*(?:TEST|BUILD)\s+FAILED\s*\*\*|"result"\s*:\s*"Failed"|Test Suite '.*' failed/;
48
+ /** Probe carriers for enforceProbeReversion on an Apple codebase. */
49
+ export const SWIFT_PROBE_FILE_PATTERN = /\.(?:swift|m|mm)$/;
50
+ //# sourceMappingURL=swift.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"swift.js","sourceRoot":"","sources":["../../rules/swift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAC7B,mDAAmD,CAAA;AAErD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,4CAA4C,CAAA;AAE9E;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAClC,uFAAuF,CAAA;AAEzF,MAAM,CAAC,MAAM,yBAAyB,GACpC,6EAA6E,CAAA;AAE/E,MAAM,CAAC,MAAM,sBAAsB,GACjC,sFAAsF,CAAA;AAExF,qEAAqE;AACrE,MAAM,CAAC,MAAM,wBAAwB,GAAG,mBAAmB,CAAA"}
@@ -0,0 +1,36 @@
1
+ import type { Rule } from '@nizos/probity';
2
+ export declare function extractGlossaryTerms(content: string): string[];
3
+ /**
4
+ * A term is "used" when it appears as a phrase in prose (case-
5
+ * insensitive, whitespace-tolerant) or as an identifier in code —
6
+ * PascalCase, camelCase, or snake_case concatenations of its words
7
+ * ("Delivery Window" → DeliveryWindow, deliveryWindow,
8
+ * delivery_window).
9
+ */
10
+ export declare function termUsagePattern(term: string): RegExp;
11
+ /**
12
+ * Surfaces glossary drift at the moment it is created: when a write
13
+ * to the glossary removes or renames a term that specs, tests, or
14
+ * source code still use, the write is blocked with the list of files
15
+ * using the term — rename the usages in the same change (or record
16
+ * the old term's fate in the glossary entry) and re-apply. Removing
17
+ * a term nothing uses passes silently, as do additions and
18
+ * definition edits.
19
+ *
20
+ * The companion invariant — new spec/port vocabulary conforming to
21
+ * the glossary — is judged by `enforceAcceptanceLanguage` and
22
+ * `enforcePortsBoundary` when given `glossaryPath`.
23
+ *
24
+ * Applies to: write actions to the glossary file (scope via a
25
+ * `{ files, rules }` block on its path). Deterministic — no AI call.
26
+ *
27
+ * @param options.searchRoots — absolute paths to scan for usages
28
+ * (repo root is fine; node_modules/build dirs are skipped).
29
+ * @param options.searchPattern — which files count as usage sites
30
+ * (default: common source, spec, and doc extensions).
31
+ */
32
+ export declare function surfaceGlossaryTermBreakage(options: {
33
+ searchRoots: string[];
34
+ searchPattern?: RegExp;
35
+ }): Rule;
36
+ //# sourceMappingURL=ubiquitous-language.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ubiquitous-language.d.ts","sourceRoot":"","sources":["../../rules/ubiquitous-language.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAU,IAAI,EAA2B,MAAM,gBAAgB,CAAA;AAiB3E,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAE9D;AAMD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAcrD;AA+BD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE;IACnD,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,GAAG,IAAI,CA8CP"}