@sabaiway/agent-workflow-memory 1.11.1 → 1.12.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.
- package/CHANGELOG.md +20 -0
- package/SKILL.md +14 -7
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/scripts/archive-decisions.mjs +39 -2
- package/references/scripts/archive-decisions.test.mjs +118 -1
- package/references/scripts/check-docs-size.mjs +50 -24
- package/references/scripts/check-docs-size.test.mjs +73 -1
- package/references/templates/agent_rules.md +1 -0
- package/references/templates/verification-profile.json +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@ All notable changes to the memory substrate. Versions are this **package's** npm
|
|
|
4
4
|
they are distinct from the **deployment-lineage** stamp written into a project's
|
|
5
5
|
`docs/ai/.memory-version` (which tracks the shared `agent-workflow` lineage, head `1.3.0`).
|
|
6
6
|
|
|
7
|
+
## 1.12.0 — Verification-profile template + the docs-index-on-rotation regen (BUGFREE-3, AD-049)
|
|
8
|
+
|
|
9
|
+
A **minor** release (deployment-lineage head stays `1.3.0` — no migration; co-released with the
|
|
10
|
+
workflow kit 1.40.0). The memory substrate gains the optional verification-profile config and closes
|
|
11
|
+
the docs-index-goes-stale-on-rotation cost (economics item (h)).
|
|
12
|
+
|
|
13
|
+
- **Verification-profile template** — a new seeded `references/templates/verification-profile.json`
|
|
14
|
+
(`schema:1`; the kit reads it, kit 1.40.0) is created on bootstrap and ensured-if-missing on
|
|
15
|
+
upgrade (the `gates.json` / `orchestration.json` twin); `SKILL.md`'s bootstrap/ensure prose now
|
|
16
|
+
names it. An **absent profile reproduces today's V8 + node:test behaviour exactly** — it only ADDS
|
|
17
|
+
an opt-in default a consumer edits for its own language/runner.
|
|
18
|
+
- **(h) a rotation regenerates `docs/ai/index.md`** — `references/scripts/archive-decisions.mjs`
|
|
19
|
+
regenerates the docs index after a successful rotation write (moves OR a normalize-only rewrite) by
|
|
20
|
+
reusing the now **root-parameterized** `check-docs-size.mjs --write-index --report` (the `--report`
|
|
21
|
+
isolates the index-write outcome from the docs-cap-check, so a benign over-cap sibling never reads
|
|
22
|
+
as a regeneration failure), with a loud instruct on absence/failure. An ADR rotation no longer
|
|
23
|
+
leaves the index stale to trip the `--check-index` gate mid-release-matrix.
|
|
24
|
+
- **§2.2 minimal-comments** — the `agent_rules` template states comments as minimal / only-vital,
|
|
25
|
+
a BASELINE a consumer project may tighten (e.g. comments forbidden entirely).
|
|
26
|
+
|
|
7
27
|
## 1.11.1 — One batched setup prompt (the F11 ask reword; AD-042)
|
|
8
28
|
|
|
9
29
|
A **patch** release (prose reword only; deployment-lineage head stays `1.3.0` — no migration).
|
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: agent-workflow-memory
|
|
|
3
3
|
description: Deploy or upgrade a portable AI-agent memory substrate in any project — an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) and a structured `docs/ai/` context store with cap/archive/index enforcement. Use when the user wants to bootstrap `docs/ai/`, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-memory` / `/agent-workflow-memory upgrade`. Triggers on "set up the memory system", "deploy the AI memory here", "bootstrap docs/ai", "upgrade the memory substrate". This is the substrate only — the workflow methodology (plan→execute→review, queue, Cleanup) is owned elsewhere and injected into AGENTS.md by the family composition root.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
metadata:
|
|
6
|
-
version: '1.
|
|
6
|
+
version: '1.12.0'
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# agent-workflow-memory
|
|
@@ -49,6 +49,7 @@ What this substrate owns vs what it only points at. The methodology + orchestrat
|
|
|
49
49
|
| Orchestration recipes (Solo / Reviewed / Council / Delegated) | **methodology engine** (not this skill) | the empty `workflow:orchestration` slot — filled by the composition root |
|
|
50
50
|
| Per-project recipe **CONFIG** (which recipe each activity/slot uses) | **memory** seeds an *editable default* | `docs/ai/orchestration.json` (agent-writable via the composition root's `set-recipe` writer, or hand-edited; the recipe **canon** + the slot **vocabulary** live in the engine / composition root, never here) |
|
|
51
51
|
| Per-project **gate declaration** (which verification commands must be green) | **memory** seeds an *editable default* | `docs/ai/gates.json` (hand-editable; an empty list as shipped — the project declares its own commands; the **runner** lives in the composition root, never here) |
|
|
52
|
+
| Per-project **verification profile** (optional; the fold-completeness language-independence contract) | **memory** seeds an *editable default* | `docs/ai/verification-profile.json` (hand-editable; the seeded default reproduces the composition root's default V8 + node:test behaviour — delete it for exactly that; declares coverage source / single-test format / optional SARIF path; the **runner** lives in the composition root, never here) |
|
|
52
53
|
|
|
53
54
|
---
|
|
54
55
|
|
|
@@ -99,11 +100,14 @@ bootstrapping over a live system, but the user makes the final call.
|
|
|
99
100
|
(`ln -s AGENTS.md CLAUDE.md`). **Leave BOTH pointer slots (`workflow:methodology` +
|
|
100
101
|
`workflow:orchestration`) exactly as shipped — empty.** Filling them is the composition root's job.
|
|
101
102
|
6. **Deploy `docs/ai/`.** Create the files + `pages/` from
|
|
102
|
-
`${CLAUDE_SKILL_DIR}/references/templates/` (every non-`AGENTS.md` template, including the
|
|
103
|
+
`${CLAUDE_SKILL_DIR}/references/templates/` (every non-`AGENTS.md` template, including the three
|
|
103
104
|
seeded, **user-editable** strict-JSON configs: `docs/ai/orchestration.json` — the per-project
|
|
104
|
-
recipe defaults the composition root's `procedures` advisor reads —
|
|
105
|
+
recipe defaults the composition root's `procedures` advisor reads — `docs/ai/gates.json` —
|
|
105
106
|
the project's gate declaration, an empty list to fill with its own verification commands,
|
|
106
|
-
consumed by the composition root's gate runner
|
|
107
|
+
consumed by the composition root's gate runner — and `docs/ai/verification-profile.json` — the
|
|
108
|
+
OPTIONAL fold-completeness verification profile (the language-independence contract; the seeded
|
|
109
|
+
default reproduces the composition root's default V8 + node:test behaviour, delete it for exactly
|
|
110
|
+
that)). Keep each `.md` file's frontmatter.
|
|
107
111
|
7. **Fill templates** per the table below.
|
|
108
112
|
8. **Install enforcement (Node projects).** Copy `${CLAUDE_SKILL_DIR}/references/scripts/*.mjs`
|
|
109
113
|
(+ `*.test.mjs`) into the project's `scripts/`. **No Node runtime** → skip this + the hook;
|
|
@@ -171,13 +175,16 @@ Fill strategy:
|
|
|
171
175
|
**untracked AND not ignored** → **AMBIGUOUS** → **ASK** the user before writing. This visibility check
|
|
172
176
|
runs on **every** in-range upgrade, even at head — it is not gated by the stamp delta, but it is gated
|
|
173
177
|
**behind** the never-downgrade STOP above. **Also stamp-independent (same gate, before the equal-head
|
|
174
|
-
short-circuit): ensure
|
|
175
|
-
`docs/ai/gates.json`, **create the file from its
|
|
178
|
+
short-circuit): ensure the THREE seeded `.json` configs** — for `docs/ai/orchestration.json`,
|
|
179
|
+
`docs/ai/gates.json`, AND `docs/ai/verification-profile.json`, **create the file from its
|
|
176
180
|
`${CLAUDE_SKILL_DIR}/references/templates/` template if missing**, **preserve it byte-for-byte
|
|
177
181
|
if it already exists** (a user may have edited it; never clobber it). The shipped
|
|
178
182
|
`orchestration.json` template's `_README` already frames that config as agent-writable (set it
|
|
179
183
|
with the `set-recipe` writer) and still hand-editable; `gates.json` is the project's own gate
|
|
180
|
-
declaration (what to verify — consumed by the composition root's gate runner)
|
|
184
|
+
declaration (what to verify — consumed by the composition root's gate runner);
|
|
185
|
+
`verification-profile.json` is the OPTIONAL fold-completeness verification profile — the seeded
|
|
186
|
+
default reproduces the composition root's default V8 + node:test behaviour, so a project deletes
|
|
187
|
+
it to fall back to exactly that, or edits it for another language/runner. (Refreshing the
|
|
181
188
|
orchestration `_README` note in place on an existing file is the **composition root's** job on
|
|
182
189
|
its own reconcile — this substrate is standalone and only seeds-or-preserves; it owns no
|
|
183
190
|
cross-package refresh helper. The gates declaration gets no note-refresh at all — authored
|
package/capability.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-memory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "Portable, cross-agent memory substrate for AI coding agents — an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement, deployable standalone or as part of the agent-workflow family. The memory layer of the agent-workflow family.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
// oldest (lowest AD id, top of file) first; an entry's lines move verbatim.
|
|
11
11
|
//
|
|
12
12
|
// Modes:
|
|
13
|
-
// (default) rotate, mutate files in place (only when something is over cap)
|
|
13
|
+
// (default) rotate, mutate files in place (only when something is over cap), then regenerate
|
|
14
|
+
// docs/ai/index.md so the rotation never leaves the index stale (item (h))
|
|
14
15
|
// --dry-run print the planned move-set, change nothing
|
|
15
16
|
// --check report per-tier lines/cap; exit 1 if any tier is over its cap
|
|
16
17
|
// --today=YYYY-MM-DD pin the lastUpdated stamp (tests / reproducible runs)
|
|
@@ -41,10 +42,40 @@
|
|
|
41
42
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
42
43
|
import { dirname, resolve } from 'node:path';
|
|
43
44
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
45
|
+
import { spawnSync } from 'node:child_process';
|
|
44
46
|
|
|
45
47
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
46
48
|
const DEFAULT_ROOT = resolve(__dirname, '..');
|
|
47
49
|
|
|
50
|
+
// (h) — after a rotation write, docs/ai/index.md would silently go stale (a moved ADR, or just the
|
|
51
|
+
// bumped lastUpdated), tripping the SEPARATE `--check-index` gate mid-release-matrix. So the
|
|
52
|
+
// rotation regenerates the index by REUSING the ONE generator in the sibling check-docs-size.mjs
|
|
53
|
+
// (spawned with --write-index --root=<root>). The subprocess bridges that script's ASYNC generator,
|
|
54
|
+
// so this runCli stays SYNCHRONOUS (spawnSync) — the existing sync callers/tests never ripple. It
|
|
55
|
+
// DEGRADES LOUDLY to an instruct (never a silent failure) when the sibling is absent or the
|
|
56
|
+
// regeneration fails; the --check-index gate still catches a stale index. Injectable for tests.
|
|
57
|
+
const CHECK_DOCS_SIBLING = resolve(__dirname, 'check-docs-size.mjs');
|
|
58
|
+
const INDEX_INSTRUCT = 'run `node scripts/check-docs-size.mjs --write-index` to refresh docs/ai/index.md';
|
|
59
|
+
// Exported + its filesystem edges injectable (deps) so BOTH degrade branches are unit-testable.
|
|
60
|
+
export const defaultRegenerateIndex = (root, today, deps = {}) => {
|
|
61
|
+
const exists = deps.existsSync ?? existsSync;
|
|
62
|
+
const spawn = deps.spawnSync ?? spawnSync;
|
|
63
|
+
const sibling = deps.sibling ?? CHECK_DOCS_SIBLING;
|
|
64
|
+
if (!exists(sibling)) {
|
|
65
|
+
return { ok: false, detail: `the index generator is not beside this script — ${INDEX_INSTRUCT}` };
|
|
66
|
+
}
|
|
67
|
+
// `--report` ISOLATES the index-WRITE outcome from the docs-cap-CHECK outcome: check-docs-size
|
|
68
|
+
// --write-index still WRITES the index (and still exits 2 on an empty write / rejects a genuine
|
|
69
|
+
// throw), but --report suppresses its exit-1 on an unrelated over-cap co-located doc — otherwise a
|
|
70
|
+
// benign over-cap sibling would read as an index-regeneration FAILURE (a cry-wolf on this very
|
|
71
|
+
// loud-degrade channel).
|
|
72
|
+
const r = spawn(process.execPath, [sibling, '--write-index', '--report', `--root=${root}`, `--today=${today}`], { encoding: 'utf8' });
|
|
73
|
+
if (r.error || r.status !== 0) {
|
|
74
|
+
return { ok: false, detail: `index regeneration failed (${(r.error && r.error.message) || `exit ${r.status}`}) — ${INDEX_INSTRUCT}` };
|
|
75
|
+
}
|
|
76
|
+
return { ok: true, detail: (r.stdout || '').trim() };
|
|
77
|
+
};
|
|
78
|
+
|
|
48
79
|
export const HOT_REL = 'docs/ai/decisions.md';
|
|
49
80
|
export const WARM_REL = 'docs/ai/history/decisions-archive.md';
|
|
50
81
|
export const COLD_REL = 'docs/ai/history/decisions-archive-early.md';
|
|
@@ -295,7 +326,7 @@ const parseArgs = (argv) => {
|
|
|
295
326
|
};
|
|
296
327
|
|
|
297
328
|
export const runCli = (argv, deps = {}) => {
|
|
298
|
-
const { root = DEFAULT_ROOT, log = console.log, logError = console.error } = deps;
|
|
329
|
+
const { root = DEFAULT_ROOT, log = console.log, logError = console.error, regenerateIndex = defaultRegenerateIndex } = deps;
|
|
299
330
|
try {
|
|
300
331
|
const { flags, today: todayOpt } = parseArgs(argv);
|
|
301
332
|
if (flags.help) {
|
|
@@ -406,6 +437,10 @@ export const runCli = (argv, deps = {}) => {
|
|
|
406
437
|
mkdirSync(dirname(tier.path), { recursive: true });
|
|
407
438
|
writeFileSync(tier.path, renderTier(updated, entries), 'utf8');
|
|
408
439
|
}
|
|
440
|
+
// (h) — the write loop completed (a full successful rotation OR a normalize-only rewrite): the
|
|
441
|
+
// docs index is now stale, so regenerate it here (never on --check / --dry-run / the
|
|
442
|
+
// nothing-to-rotate no-op / a pre-write refusal — those return before this point).
|
|
443
|
+
const regen = regenerateIndex(root, today);
|
|
409
444
|
log('[archive-decisions] rotated:');
|
|
410
445
|
log(` HOT→WARM: ${summary.hotToWarm.join(', ') || '(none)'}`);
|
|
411
446
|
log(` WARM→COLD: ${summary.warmToCold.join(', ') || '(none)'}`);
|
|
@@ -413,6 +448,8 @@ export const runCli = (argv, deps = {}) => {
|
|
|
413
448
|
log(` normalize-only rewrite (over cap on raw lines, no entry moves): ${summary.normalizeOnly.join(', ')}`);
|
|
414
449
|
}
|
|
415
450
|
log(` now: HOT ${summary.after.hot} · WARM ${summary.after.warm} · COLD ${summary.after.cold}`);
|
|
451
|
+
if (regen.ok) log(' regenerated docs/ai/index.md (the rotation kept the index fresh)');
|
|
452
|
+
else logError(`[archive-decisions] docs/ai/index.md NOT regenerated — ${regen.detail}`);
|
|
416
453
|
return 0;
|
|
417
454
|
} catch (err) {
|
|
418
455
|
logError(`[archive-decisions] ${err.message}`);
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
planRotation,
|
|
15
15
|
updateRangeTokens,
|
|
16
16
|
runCli,
|
|
17
|
+
defaultRegenerateIndex,
|
|
17
18
|
} from './archive-decisions.mjs';
|
|
18
19
|
|
|
19
20
|
// Hermetic by design: this test ships as deploy payload and runs inside CONSUMER repos via the
|
|
@@ -67,10 +68,14 @@ const seedProject = (root, { hotIds, warmIds, coldIds, hotCapDelta = 100, warmCa
|
|
|
67
68
|
};
|
|
68
69
|
};
|
|
69
70
|
|
|
71
|
+
// The existing rotation tests are about ROTATION, not the index-regen hook (item (h)) — inject a
|
|
72
|
+
// no-op regenerator so they stay hermetic + fast (no real check-docs-size subprocess). The hook's
|
|
73
|
+
// own firing/degrade behavior is pinned by the dedicated (h) describe below; the real end-to-end
|
|
74
|
+
// spawn is isolated to one integration test there.
|
|
70
75
|
const run = (argv, root) => {
|
|
71
76
|
const out = [];
|
|
72
77
|
const err = [];
|
|
73
|
-
const code = runCli(argv, { root, log: (l) => out.push(l), logError: (l) => err.push(l) });
|
|
78
|
+
const code = runCli(argv, { root, log: (l) => out.push(l), logError: (l) => err.push(l), regenerateIndex: () => ({ ok: true, detail: '' }) });
|
|
74
79
|
return { code, out, err, text: out.join('\n'), errText: err.join('\n') };
|
|
75
80
|
};
|
|
76
81
|
|
|
@@ -363,3 +368,115 @@ describe('created tiers (a consumer with no history files yet)', () => {
|
|
|
363
368
|
assert.match(errText, /Unknown argument/);
|
|
364
369
|
});
|
|
365
370
|
});
|
|
371
|
+
|
|
372
|
+
// ── (h) — a rotation regenerates docs/ai/index.md (BUGFREE-3 / AD-049) ─────────────────
|
|
373
|
+
describe('(h) index regeneration after rotation', () => {
|
|
374
|
+
const spyRun = (argv, root, regen) => {
|
|
375
|
+
const out = [];
|
|
376
|
+
const err = [];
|
|
377
|
+
const code = runCli(argv, { root, log: (l) => out.push(l), logError: (l) => err.push(l), regenerateIndex: regen });
|
|
378
|
+
return { code, out, err, text: out.join('\n'), errText: err.join('\n') };
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
it('calls the regenerator once with (root, today) AFTER a successful rotation write', () => {
|
|
382
|
+
const root = makeRoot();
|
|
383
|
+
seedProject(root, { hotIds: ['005', '006', '007', '008'], warmIds: ['003', '004'], coldIds: ['001', '002'], hotCapDelta: -1 });
|
|
384
|
+
const calls = [];
|
|
385
|
+
const { code } = spyRun(['--today=2026-01-02'], root, (r, t) => { calls.push([r, t]); return { ok: true, detail: '' }; });
|
|
386
|
+
assert.equal(code, 0);
|
|
387
|
+
assert.deepEqual(calls, [[root, '2026-01-02']], 'exactly one regen, with the rotation root + today');
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('does NOT regenerate on a no-op, --check, --dry-run, or a pre-write refusal', () => {
|
|
391
|
+
let called = 0;
|
|
392
|
+
const spy = () => { called += 1; return { ok: true, detail: '' }; };
|
|
393
|
+
// no-op (under cap)
|
|
394
|
+
const r1 = makeRoot(); seedProject(r1, { hotIds: ['005'], warmIds: ['003'], coldIds: ['001'] });
|
|
395
|
+
spyRun([], r1, spy);
|
|
396
|
+
// --check
|
|
397
|
+
spyRun(['--check'], r1, spy);
|
|
398
|
+
// --dry-run over a real rotation plan
|
|
399
|
+
const r2 = makeRoot(); seedProject(r2, { hotIds: ['005', '006'], warmIds: [], coldIds: [], hotCapDelta: -1 });
|
|
400
|
+
spyRun(['--dry-run'], r2, spy);
|
|
401
|
+
// a pre-write refusal (COLD exhausted)
|
|
402
|
+
const r3 = makeRoot(); seedProject(r3, { hotIds: ['005', '006', '007'], warmIds: ['003', '004'], coldIds: ['001', '002'], hotCapDelta: -1, warmCapDelta: 3, coldCapDelta: 3 });
|
|
403
|
+
spyRun(['--today=2026-01-02'], r3, spy);
|
|
404
|
+
assert.equal(called, 0, 'the hook fires only AFTER a real write — never on no-op/check/dry-run/refusal');
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('a NORMALIZE-ONLY rewrite (stampLastUpdated bumps lastUpdated, zero moves) still triggers regeneration', () => {
|
|
408
|
+
const root = makeRoot();
|
|
409
|
+
mkdirSync(join(root, 'docs', 'ai', 'history'), { recursive: true });
|
|
410
|
+
const blocks = [entryBlock('001'), entryBlock('002'), entryBlock('003')];
|
|
411
|
+
const body = blocks.join('\n\n---\n\n'); // the template separator shape: raw > cap, normalized fits
|
|
412
|
+
const probe = `${fm(999)}\n# ADRs\n\n${body}\n`;
|
|
413
|
+
writeFileSync(join(root, HOT_REL), `${fm(lineCountOf(probe) - 2)}\n# ADRs\n\n${body}\n`);
|
|
414
|
+
let called = 0;
|
|
415
|
+
const { code, text } = spyRun(['--today=2026-01-02'], root, () => { called += 1; return { ok: true, detail: '' }; });
|
|
416
|
+
assert.equal(code, 0, text);
|
|
417
|
+
assert.match(text, /normalize-only rewrite/);
|
|
418
|
+
assert.equal(called, 1, 'a normalize-only rewrite still leaves the index stale → regenerate');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('degrades LOUDLY to an instruct when regeneration fails — the rotation still succeeds (exit 0)', () => {
|
|
422
|
+
const root = makeRoot();
|
|
423
|
+
seedProject(root, { hotIds: ['005', '006'], warmIds: [], coldIds: [], hotCapDelta: -1 });
|
|
424
|
+
const { code, errText } = spyRun(['--today=2026-01-02'], root, () => ({ ok: false, detail: 'the index generator is not beside this script — run `node scripts/check-docs-size.mjs --write-index` to refresh docs/ai/index.md' }));
|
|
425
|
+
assert.equal(code, 0, 'the rotation itself succeeded — regen is best-effort');
|
|
426
|
+
assert.match(errText, /NOT regenerated/);
|
|
427
|
+
assert.match(errText, /check-docs-size\.mjs --write-index/, 'the loud instruct names the recovery command');
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('the REAL default regenerator writes docs/ai/index.md end-to-end (no injection)', () => {
|
|
431
|
+
const root = makeRoot();
|
|
432
|
+
seedProject(root, { hotIds: ['005', '006', '007', '008'], warmIds: ['003', '004'], coldIds: ['001', '002'], hotCapDelta: -1 });
|
|
433
|
+
const out = [];
|
|
434
|
+
const code = runCli(['--today=2026-01-02'], { root, log: (l) => out.push(l), logError: (l) => out.push(l) });
|
|
435
|
+
assert.equal(code, 0, out.join('\n'));
|
|
436
|
+
assert.ok(existsSync(join(root, 'docs', 'ai', 'index.md')), 'the index was regenerated by the sibling check-docs-size.mjs');
|
|
437
|
+
const idx = readFileSync(join(root, 'docs', 'ai', 'index.md'), 'utf8');
|
|
438
|
+
assert.match(idx, /decisions\.md/, 'the regenerated index lists the rotated ADR file');
|
|
439
|
+
assert.match(out.join('\n'), /regenerated docs\/ai\/index\.md/, 'the success log line fires');
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// The index-write outcome must be ISOLATED from the docs-cap-CHECK outcome: check-docs-size
|
|
443
|
+
// --write-index exits 1 on ANY over-cap co-located doc even after writing the index correctly, so
|
|
444
|
+
// a benign over-cap sibling must NEVER read as an index-regeneration failure (cry-wolf on the very
|
|
445
|
+
// loud-degrade channel item (h) is built around). The regen spawns --write-index --report.
|
|
446
|
+
it('the REAL regenerator succeeds even when an UNRELATED co-located doc is over its cap (no cry-wolf)', () => {
|
|
447
|
+
const root = makeRoot();
|
|
448
|
+
seedProject(root, { hotIds: ['005', '006', '007', '008'], warmIds: ['003', '004'], coldIds: ['001', '002'], hotCapDelta: -1 });
|
|
449
|
+
// An unrelated doc over its OWN maxLines cap: --write-index writes the index fine, but without
|
|
450
|
+
// --report it would exit 1 on this file → a false "NOT regenerated" instruct.
|
|
451
|
+
writeFileSync(join(root, 'docs', 'ai', 'handover.md'), `---\ntype: state\nlastUpdated: 2026-01-02\nscope: session\nstaleAfter: 7d\nowner: none\nmaxLines: 3\n---\n\n# H\nl1\nl2\nl3\nl4\nl5\n`);
|
|
452
|
+
const out = [];
|
|
453
|
+
const err = [];
|
|
454
|
+
const code = runCli(['--today=2026-01-02'], { root, log: (l) => out.push(l), logError: (l) => err.push(l) });
|
|
455
|
+
assert.equal(code, 0);
|
|
456
|
+
assert.match(out.join('\n'), /regenerated docs\/ai\/index\.md/, 'a benign over-cap sibling must NOT read as a regen failure');
|
|
457
|
+
assert.ok(!err.join('\n').includes('NOT regenerated'), 'no false cry-wolf on the loud-degrade channel');
|
|
458
|
+
assert.ok(existsSync(join(root, 'docs', 'ai', 'index.md')), 'the index was actually written');
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
describe('defaultRegenerateIndex — the loud-degrade branches', () => {
|
|
462
|
+
it('an absent index generator sibling → a loud instruct (ok:false)', () => {
|
|
463
|
+
const r = defaultRegenerateIndex('/tmp/anyroot', '2026-01-02', { sibling: '/nonexistent/check-docs-size.mjs' });
|
|
464
|
+
assert.equal(r.ok, false);
|
|
465
|
+
assert.match(r.detail, /not beside this script/);
|
|
466
|
+
assert.match(r.detail, /check-docs-size\.mjs --write-index/);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('a regeneration subprocess that exits nonzero → a loud instruct (ok:false)', () => {
|
|
470
|
+
const r = defaultRegenerateIndex('/tmp/anyroot', '2026-01-02', { existsSync: () => true, spawnSync: () => ({ status: 1 }) });
|
|
471
|
+
assert.equal(r.ok, false);
|
|
472
|
+
assert.match(r.detail, /index regeneration failed/);
|
|
473
|
+
assert.match(r.detail, /check-docs-size\.mjs --write-index/);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('a successful subprocess (status 0) → ok:true with the trimmed stdout', () => {
|
|
477
|
+
const r = defaultRegenerateIndex('/tmp/anyroot', '2026-01-02', { existsSync: () => true, spawnSync: () => ({ status: 0, stdout: 'Wrote docs/ai/index.md\n' }) });
|
|
478
|
+
assert.equal(r.ok, true);
|
|
479
|
+
assert.match(r.detail, /Wrote docs\/ai\/index\.md/);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
});
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
//
|
|
16
16
|
// CLI overrides:
|
|
17
17
|
// --today=YYYY-MM-DD (default today UTC) — useful for tests / reproducible runs
|
|
18
|
+
// --root=<dir> run against another project root (default this deployment) — the ADR-rotation
|
|
19
|
+
// hook passes it so a rotation regenerates the right project's index
|
|
18
20
|
// --quiet print only failures (and final summary)
|
|
19
21
|
|
|
20
22
|
import { readFile, writeFile, readdir, stat } from 'node:fs/promises';
|
|
@@ -28,6 +30,12 @@ const ROOT = resolve(__dirname, '..');
|
|
|
28
30
|
const DOCS_DIR = resolve(ROOT, 'docs/ai');
|
|
29
31
|
const INDEX_PATH = resolve(DOCS_DIR, 'index.md');
|
|
30
32
|
|
|
33
|
+
// Root-parameterized (BUGFREE-3 / AD-049, item (h)): the module ROOT constants are the CLI DEFAULT
|
|
34
|
+
// (this deployment's own root); `--root=<dir>` and the exported `regenerateIndex(root, today)`
|
|
35
|
+
// override them so the ADR-rotation hook (archive-decisions.mjs) and hermetic tests can regenerate
|
|
36
|
+
// an arbitrary root's index without ever touching the real repo tree.
|
|
37
|
+
const pathsFor = (root) => ({ root, docsDir: resolve(root, 'docs/ai'), indexPath: resolve(root, 'docs/ai/index.md') });
|
|
38
|
+
|
|
31
39
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
32
40
|
|
|
33
41
|
// Project-name + footer links for the index are auto-discovered (no hardcoding):
|
|
@@ -56,18 +64,18 @@ const walkForName = async (dir, name, acc = [], depth = 0) => {
|
|
|
56
64
|
return acc;
|
|
57
65
|
};
|
|
58
66
|
|
|
59
|
-
export const discoverMeta = async () => {
|
|
60
|
-
let projectName = basename(
|
|
67
|
+
export const discoverMeta = async (root = ROOT) => {
|
|
68
|
+
let projectName = basename(root);
|
|
61
69
|
try {
|
|
62
|
-
const pkg = JSON.parse(await readFile(resolve(
|
|
70
|
+
const pkg = JSON.parse(await readFile(resolve(root, 'package.json'), 'utf8'));
|
|
63
71
|
if (pkg.name) projectName = pkg.name;
|
|
64
72
|
} catch {
|
|
65
73
|
/* no package.json — keep dir basename */
|
|
66
74
|
}
|
|
67
|
-
const agentsFiles = await walkForName(
|
|
68
|
-
const claudeFiles = await walkForName(
|
|
69
|
-
const rootAgents = resolve(
|
|
70
|
-
const rootClaude = resolve(
|
|
75
|
+
const agentsFiles = await walkForName(root, 'AGENTS.md');
|
|
76
|
+
const claudeFiles = await walkForName(root, 'CLAUDE.md');
|
|
77
|
+
const rootAgents = resolve(root, 'AGENTS.md');
|
|
78
|
+
const rootClaude = resolve(root, 'CLAUDE.md');
|
|
71
79
|
// A subdir typically holds AGENTS.md plus a CLAUDE.md symlink to it — list each
|
|
72
80
|
// dir once (prefer AGENTS.md, drop its sibling CLAUDE.md alias).
|
|
73
81
|
const agentsDirs = new Set(agentsFiles.map((file) => dirname(resolve(file))));
|
|
@@ -78,12 +86,12 @@ export const discoverMeta = async () => {
|
|
|
78
86
|
),
|
|
79
87
|
];
|
|
80
88
|
const hierarchicalLinks = nestedFiles
|
|
81
|
-
.map((file) => relative(
|
|
89
|
+
.map((file) => relative(root, file))
|
|
82
90
|
.sort()
|
|
83
91
|
.map((rel) => `[\`${rel}\`](../../${rel})`);
|
|
84
92
|
let onDemandLinks = [];
|
|
85
93
|
try {
|
|
86
|
-
const skillDirs = await readdir(resolve(
|
|
94
|
+
const skillDirs = await readdir(resolve(root, '.agents/skills'), { withFileTypes: true });
|
|
87
95
|
onDemandLinks = skillDirs
|
|
88
96
|
.filter((dirent) => dirent.isDirectory() && /-(patterns|commands)$/.test(dirent.name))
|
|
89
97
|
.map((dirent) => dirent.name)
|
|
@@ -97,16 +105,17 @@ export const discoverMeta = async () => {
|
|
|
97
105
|
|
|
98
106
|
const parseArgs = (argv) => {
|
|
99
107
|
const flags = { report: false, writeIndex: false, checkIndex: false, quiet: false };
|
|
100
|
-
const opts = { today: null };
|
|
108
|
+
const opts = { today: null, root: null };
|
|
101
109
|
for (const arg of argv.slice(2)) {
|
|
102
110
|
if (arg === '--report') flags.report = true;
|
|
103
111
|
else if (arg === '--write-index') flags.writeIndex = true;
|
|
104
112
|
else if (arg === '--check-index') flags.checkIndex = true;
|
|
105
113
|
else if (arg === '--quiet') flags.quiet = true;
|
|
106
114
|
else if (arg.startsWith('--today=')) opts.today = arg.slice('--today='.length);
|
|
115
|
+
else if (arg.startsWith('--root=')) opts.root = arg.slice('--root='.length);
|
|
107
116
|
else if (arg === '--help' || arg === '-h') {
|
|
108
117
|
console.log(
|
|
109
|
-
'Usage: check-docs-size.mjs [--report|--write-index|--check-index] [--today=YYYY-MM-DD] [--quiet]',
|
|
118
|
+
'Usage: check-docs-size.mjs [--report|--write-index|--check-index] [--today=YYYY-MM-DD] [--root=<dir>] [--quiet]',
|
|
110
119
|
);
|
|
111
120
|
process.exit(0);
|
|
112
121
|
} else {
|
|
@@ -161,11 +170,11 @@ export const computeToday = (todayStr) =>
|
|
|
161
170
|
? new Date(`${todayStr}T00:00:00Z`)
|
|
162
171
|
: new Date(new Date().toISOString().slice(0, 10) + 'T00:00:00Z');
|
|
163
172
|
|
|
164
|
-
export const inspectFile = async (filePath, today) => {
|
|
173
|
+
export const inspectFile = async (filePath, today, root = ROOT) => {
|
|
165
174
|
const text = await readFile(filePath, 'utf8');
|
|
166
175
|
const lineCount = text.split('\n').length - (text.endsWith('\n') ? 1 : 0);
|
|
167
176
|
const fm = parseFrontmatter(text);
|
|
168
|
-
const rel = relative(
|
|
177
|
+
const rel = relative(root, filePath);
|
|
169
178
|
|
|
170
179
|
if (!fm) {
|
|
171
180
|
return {
|
|
@@ -301,24 +310,41 @@ export const checkIndexFreshness = (rows, onDiskText, meta = {}) => {
|
|
|
301
310
|
return { fresh: expected === onDiskText, expected };
|
|
302
311
|
};
|
|
303
312
|
|
|
304
|
-
const writeIndex = async (rows, today, meta) => {
|
|
313
|
+
const writeIndex = async (rows, today, meta, indexPath = INDEX_PATH) => {
|
|
305
314
|
const body = buildIndex(rows, today.toISOString().slice(0, 10), meta);
|
|
306
|
-
await writeFile(
|
|
315
|
+
await writeFile(indexPath, body, 'utf8');
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// regenerateIndex(root, todayStr) — the ONE reused generator, root-parameterized (item (h)). It runs
|
|
319
|
+
// the SAME walk → inspect → discoverMeta → writeIndex pipeline as `--write-index`, against `root`
|
|
320
|
+
// (default this deployment). The ADR-rotation hook reaches it via the CLI (`--write-index --root=…`);
|
|
321
|
+
// hermetic tests call it directly. `todayStr` is 'YYYY-MM-DD' (null → today). Returns the written
|
|
322
|
+
// index path + row count. No second index implementation exists.
|
|
323
|
+
export const regenerateIndex = async (root, todayStr = null) => {
|
|
324
|
+
const { docsDir, indexPath } = pathsFor(root);
|
|
325
|
+
const today = computeToday(todayStr);
|
|
326
|
+
const files = (await walkMarkdownFiles(docsDir)).sort();
|
|
327
|
+
const inspected = await Promise.all(files.map((f) => inspectFile(f, today, root)));
|
|
328
|
+
const rows = inspected.map(formatRow);
|
|
329
|
+
const meta = await discoverMeta(root);
|
|
330
|
+
await writeIndex(rows, today, meta, indexPath);
|
|
331
|
+
return { indexPath, files: rows.length };
|
|
307
332
|
};
|
|
308
333
|
|
|
309
334
|
const main = async () => {
|
|
310
335
|
const { flags, opts } = parseArgs(process.argv);
|
|
336
|
+
const { root, docsDir, indexPath } = pathsFor(opts.root ? resolve(opts.root) : ROOT);
|
|
311
337
|
const today = computeToday(opts.today);
|
|
312
|
-
const files = (await walkMarkdownFiles(
|
|
313
|
-
const inspected = await Promise.all(files.map((f) => inspectFile(f, today)));
|
|
338
|
+
const files = (await walkMarkdownFiles(docsDir)).sort();
|
|
339
|
+
const inspected = await Promise.all(files.map((f) => inspectFile(f, today, root)));
|
|
314
340
|
const rows = inspected.map(formatRow);
|
|
315
341
|
|
|
316
|
-
const meta = flags.writeIndex || flags.checkIndex ? await discoverMeta() : null;
|
|
342
|
+
const meta = flags.writeIndex || flags.checkIndex ? await discoverMeta(root) : null;
|
|
317
343
|
|
|
318
344
|
if (flags.writeIndex) {
|
|
319
|
-
await writeIndex(rows, today, meta);
|
|
320
|
-
console.log(`Wrote ${relative(
|
|
321
|
-
const after = await stat(
|
|
345
|
+
await writeIndex(rows, today, meta, indexPath);
|
|
346
|
+
console.log(`Wrote ${relative(root, indexPath)}`);
|
|
347
|
+
const after = await stat(indexPath);
|
|
322
348
|
if (after.size === 0) {
|
|
323
349
|
console.error('index.md was written empty');
|
|
324
350
|
process.exit(2);
|
|
@@ -326,16 +352,16 @@ const main = async () => {
|
|
|
326
352
|
}
|
|
327
353
|
|
|
328
354
|
if (flags.checkIndex) {
|
|
329
|
-
const onDisk = existsSync(
|
|
355
|
+
const onDisk = existsSync(indexPath) ? await readFile(indexPath, 'utf8') : null;
|
|
330
356
|
const { fresh } = checkIndexFreshness(rows, onDisk, meta);
|
|
331
357
|
if (!fresh) {
|
|
332
358
|
console.error(
|
|
333
|
-
`[check-docs-size] FAIL: ${relative(
|
|
359
|
+
`[check-docs-size] FAIL: ${relative(root, indexPath)} is stale (out of sync with source frontmatter). Regenerate the index (--write-index) and commit the regenerated file.`,
|
|
334
360
|
);
|
|
335
361
|
process.exit(1);
|
|
336
362
|
}
|
|
337
363
|
console.log(
|
|
338
|
-
`[check-docs-size] OK — ${relative(
|
|
364
|
+
`[check-docs-size] OK — ${relative(root, indexPath)} is in sync with source frontmatter.`,
|
|
339
365
|
);
|
|
340
366
|
return;
|
|
341
367
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
2
2
|
import { expect } from './_expect-shim.mjs';
|
|
3
|
-
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { mkdtemp, writeFile, mkdir, readFile, rm } from 'node:fs/promises';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
4
6
|
import { tmpdir } from 'node:os';
|
|
5
7
|
import { join } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
6
9
|
import {
|
|
7
10
|
parseFrontmatter,
|
|
8
11
|
parseStaleAfter,
|
|
@@ -11,6 +14,7 @@ import {
|
|
|
11
14
|
buildIndex,
|
|
12
15
|
checkIndexFreshness,
|
|
13
16
|
walkMarkdownFiles,
|
|
17
|
+
regenerateIndex,
|
|
14
18
|
} from './check-docs-size.mjs';
|
|
15
19
|
|
|
16
20
|
describe('parseFrontmatter', () => {
|
|
@@ -202,3 +206,71 @@ describe('checkIndexFreshness', () => {
|
|
|
202
206
|
expect(checkIndexFreshness(rows, onDisk).fresh).toBe(true);
|
|
203
207
|
});
|
|
204
208
|
});
|
|
209
|
+
|
|
210
|
+
// ── (h) — root-parameterization: the ADR-rotation hook regenerates ANOTHER root's index ────
|
|
211
|
+
// The generator's module ROOT is the CLI default only; --root / regenerateIndex(root) target an
|
|
212
|
+
// arbitrary tree so the rotation hook (and hermetic tests) never touch the real repo.
|
|
213
|
+
describe('root parameterization (item (h))', () => {
|
|
214
|
+
let root;
|
|
215
|
+
beforeEach(async () => {
|
|
216
|
+
root = await mkdtemp(join(tmpdir(), 'check-docs-root-'));
|
|
217
|
+
await mkdir(join(root, 'docs', 'ai'), { recursive: true });
|
|
218
|
+
});
|
|
219
|
+
afterEach(async () => {
|
|
220
|
+
await rm(root, { recursive: true, force: true });
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const seedDoc = (name, extra = '') =>
|
|
224
|
+
writeFile(join(root, 'docs', 'ai', name), `---\ntype: reference\nlastUpdated: 2026-07-08\nscope: permanent\nstaleAfter: 30d\nowner: none\nmaxLines: 100\n---\n\n# ${name}\n${extra}`);
|
|
225
|
+
|
|
226
|
+
it('inspectFile computes the file path RELATIVE to the passed root (not the module ROOT)', async () => {
|
|
227
|
+
await seedDoc('handover.md');
|
|
228
|
+
const result = await inspectFile(join(root, 'docs', 'ai', 'handover.md'), computeToday('2026-07-08'), root);
|
|
229
|
+
expect(result.path).toBe('docs/ai/handover.md');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('regenerateIndex(root, today) writes THAT root\'s docs/ai/index.md from its frontmatter', async () => {
|
|
233
|
+
await seedDoc('a.md');
|
|
234
|
+
await seedDoc('b.md');
|
|
235
|
+
const res = await regenerateIndex(root, '2026-07-08');
|
|
236
|
+
expect(res.indexPath).toBe(join(root, 'docs', 'ai', 'index.md'));
|
|
237
|
+
expect(existsSync(res.indexPath)).toBe(true);
|
|
238
|
+
const index = await readFile(res.indexPath, 'utf8');
|
|
239
|
+
expect(index).toMatch(/lastUpdated: 2026-07-08/); // header date is the argument
|
|
240
|
+
expect(index).toMatch(/a\.md/);
|
|
241
|
+
expect(index).toMatch(/b\.md/);
|
|
242
|
+
expect(index).not.toMatch(/\[`index\.md`\]/); // the index never lists itself
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('a re-run with unchanged sources is byte-identical (deterministic, --check-index safe)', async () => {
|
|
246
|
+
await seedDoc('a.md');
|
|
247
|
+
const first = await regenerateIndex(root, '2026-07-08');
|
|
248
|
+
const bytesA = await readFile(first.indexPath, 'utf8');
|
|
249
|
+
await regenerateIndex(root, '2026-07-08');
|
|
250
|
+
const bytesB = await readFile(first.indexPath, 'utf8');
|
|
251
|
+
expect(bytesB).toBe(bytesA);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// The CLI entry (main) over --root — a subprocess smoke so the --help usage + the --check-index
|
|
255
|
+
// fresh/stale branches (root-parameterized) are exercised end-to-end.
|
|
256
|
+
const SCRIPT = fileURLToPath(new URL('./check-docs-size.mjs', import.meta.url));
|
|
257
|
+
const runCli = (args) => spawnSync(process.execPath, [SCRIPT, ...args], { encoding: 'utf8' });
|
|
258
|
+
|
|
259
|
+
it('--help prints the usage (naming --root) and exits 0', () => {
|
|
260
|
+
const r = runCli(['--help']);
|
|
261
|
+
expect(r.status).toBe(0);
|
|
262
|
+
expect(r.stdout).toMatch(/--root=/);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('--check-index --root: a fresh index is OK (exit 0); a drifted one is stale (exit 1)', async () => {
|
|
266
|
+
await seedDoc('a.md');
|
|
267
|
+
expect(runCli(['--write-index', `--root=${root}`]).status).toBe(0);
|
|
268
|
+
const fresh = runCli(['--check-index', `--root=${root}`]);
|
|
269
|
+
expect(fresh.status).toBe(0);
|
|
270
|
+
expect(fresh.stdout).toMatch(/in sync/);
|
|
271
|
+
await seedDoc('b.md'); // a new source row drifts the on-disk index
|
|
272
|
+
const stale = runCli(['--check-index', `--root=${root}`]);
|
|
273
|
+
expect(stale.status).toBe(1);
|
|
274
|
+
expect(stale.stderr).toMatch(/stale/);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
@@ -55,6 +55,7 @@ Before proposing changes or committing, review against:
|
|
|
55
55
|
### 2.2. Clean Code
|
|
56
56
|
- **No magic literals** — extract string/numeric constants to named consts at module level.
|
|
57
57
|
- **DRY** — no duplicated logic.
|
|
58
|
+
- **Minimal comments (a BASELINE this project may tighten)** — if this project sets a stricter rule (e.g. comments forbidden entirely), that stricter rule ALWAYS wins; this is only a floor. Otherwise comment only where vitally necessary (a non-obvious invariant, a fail-closed rationale, a subtle edge). Make the code self-explaining first — clear variable/function names and compact-but-unambiguous test descriptions replace most comments; never restate what the code already says.
|
|
58
59
|
|
|
59
60
|
### 2.3. Strict Compliance
|
|
60
61
|
- Only `const` (no `let`); no classes — pure functions, closures, modules.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_README": "Optional per-project VERIFICATION PROFILE for the fold-completeness gate (the language-independence contract). DELETE this file to reproduce the exact default behaviour (V8 line coverage + node:test TAP on stdout) — an absent profile is fully supported. Present, it GENERALIZES three inputs so a consumer on another language/runner can drive the same gate: (1) coverage.kind is \"v8\" (default) or \"lcov\" — with lcov, set coverage.lcovPath to where your suite leaves an LCOV file; (2) singleTest.argv is the shell-free command template for probing ONE test (placeholders {file} and {pattern} are required; a file-based resultFormat also requires {resultPath}), and singleTest.resultFormat is \"tap-stdout\" (default), \"tap-file\", or \"junit-xml\"; (3) findings.sarifPath (optional) points at a SARIF file for advisory-only findings (never gate-blocking). The suite COMMAND is NOT declared here — it stays your docs/ai/gates.json unit-tests gate (so the fold run and the gate share command-identity). Every DECLARED path (coverage.lcovPath, findings.sarifPath) MUST be gitignored or outside the repo (a symlink is refused): an in-tree, non-ignored file the suite writes would move the review fingerprint. Env knobs still override (AW_FOLD_SUITE_CMD / AW_FOLD_BOUND_CMD / AW_FOLD_RESULTS). Strict JSON — no comments.",
|
|
3
|
+
"schema": 1,
|
|
4
|
+
"coverage": { "kind": "v8" },
|
|
5
|
+
"singleTest": {
|
|
6
|
+
"argv": ["node", "--test", "--test-reporter", "tap", "--test-name-pattern={pattern}", "{file}"],
|
|
7
|
+
"resultFormat": "tap-stdout"
|
|
8
|
+
},
|
|
9
|
+
"findings": {}
|
|
10
|
+
}
|