arkgate 4.8.15 → 4.8.17

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 (69) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +7 -5
  3. package/bin/ark-check-runtime.mjs +5 -1
  4. package/bin/ark-mcp-runtime.mjs +14 -7
  5. package/bin/ark-shared.mjs +2 -0
  6. package/bin/ark.mjs +17 -8
  7. package/bin/lib/adr-path.mjs +116 -0
  8. package/bin/lib/adr-presence.mjs +3 -2
  9. package/bin/lib/analysis-engine.mjs +6 -6
  10. package/bin/lib/architecture-scan.mjs +17 -0
  11. package/bin/lib/baseline-key.mjs +2 -0
  12. package/bin/lib/config-contract.mjs +1 -1
  13. package/bin/lib/diagnostic-catalog.mjs +2 -0
  14. package/bin/lib/doctor-advisories.mjs +26 -0
  15. package/bin/lib/doctor-green-cite.mjs +139 -0
  16. package/bin/lib/doctor-human.mjs +98 -63
  17. package/bin/lib/doctor-next-actions.mjs +9 -0
  18. package/bin/lib/doctor-plan.mjs +6 -3
  19. package/bin/lib/field-install.mjs +47 -7
  20. package/bin/lib/first-run-help.mjs +3 -1
  21. package/bin/lib/improvement-compass-doctor.mjs +3 -1
  22. package/bin/lib/improvement-compass-map.mjs +3 -1
  23. package/bin/lib/invariant-coverage.mjs +121 -0
  24. package/bin/lib/invariant-tests-path.mjs +212 -0
  25. package/bin/lib/package-manager.mjs +8 -0
  26. package/bin/lib/policy-delta-io.mjs +19 -10
  27. package/bin/lib/prototype-shortcuts.mjs +224 -0
  28. package/bin/lib/remediation.mjs +11 -0
  29. package/bin/lib/start-preview.mjs +20 -1
  30. package/dist/{configTypes-Dt3DpVbd.d.ts → configTypes-VD0qcubY.d.ts} +2 -1
  31. package/dist/{diagnosticCatalog-BEg85XlE.d.ts → diagnosticCatalog-DVABouBV.d.ts} +30 -3
  32. package/dist/eslint/index.cjs +4 -4
  33. package/dist/eslint/index.d.ts +1 -1
  34. package/dist/eslint/index.js +4 -4
  35. package/dist/index.cjs +31 -31
  36. package/dist/index.d.ts +5 -5
  37. package/dist/index.js +31 -31
  38. package/dist/nestjs/index.cjs +1 -1
  39. package/dist/nestjs/index.d.ts +3 -3
  40. package/dist/nestjs/index.js +1 -1
  41. package/dist/runtime/index.cjs +10 -10
  42. package/dist/runtime/index.d.ts +6 -6
  43. package/dist/runtime/index.js +10 -10
  44. package/dist/{types-TBiv0WHL.d.ts → types-BSzRy2X1.d.ts} +1 -1
  45. package/dist/{types-CN9tVMPz.d.ts → types-D5GT5ZT8.d.ts} +1 -1
  46. package/docs/README.md +1 -1
  47. package/docs/agent-guide.md +3 -1
  48. package/docs/configuration.md +17 -6
  49. package/docs/develop.md +3 -1
  50. package/docs/diagnostics.md +25 -1
  51. package/docs/enthusiast/how-to-agent-gates.md +3 -1
  52. package/docs/package-surface.md +2 -2
  53. package/docs/use.md +5 -3
  54. package/package.json +1 -1
  55. package/schemas/ark.config.schema.json +1 -1
  56. package/server.json +2 -2
  57. package/templates/agent-skills/README.md +1 -1
  58. package/templates/agent-skills/ark-adopt/SKILL.md +19 -2
  59. package/templates/agent-skills/ark-autopilot/SKILL.md +6 -1
  60. package/templates/agent-skills/ark-coverage/SKILL.md +1 -1
  61. package/templates/agent-skills/ark-explain/SKILL.md +2 -1
  62. package/templates/agent-skills/ark-explore/SKILL.md +22 -1
  63. package/templates/agent-skills/ark-place/SKILL.md +4 -0
  64. package/templates/skills/ark-adopt.md +19 -2
  65. package/templates/skills/ark-autopilot.md +6 -1
  66. package/templates/skills/ark-coverage.md +1 -1
  67. package/templates/skills/ark-explain.md +2 -1
  68. package/templates/skills/ark-explore.md +22 -1
  69. package/templates/skills/ark-place.md +4 -0
package/CHANGELOG.md CHANGED
@@ -96,6 +96,71 @@ in the immutable pre-2.0 archive linked below.
96
96
  Existing configs keep whatever URL they already have. Editor completion
97
97
  matches the 4.x line you just installed.
98
98
 
99
+ ## 4.8.17 — 2026-09-17
100
+
101
+ **Patch** over **4.8.16**. Daily accumulate of the two ships that landed
102
+ after 4.8.16 published: ADR path on policy weaken / new layer edge
103
+ ([#264](https://github.com/pedroknigge/arkgate/pull/264)) and the
104
+ policy-delta-ack-match confidence retarget
105
+ ([#265](https://github.com/pedroknigge/arkgate/pull/265)).
106
+ **Write. Check. Ship.** **No required config migration.** No
107
+ `schemaVersion` bump. Does not close `K01` / `Z09`. This mother
108
+ `ark.config.json` still does **not** turn `arkOrder` on.
109
+
110
+ **Status: prepared** (npm `latest` remains **4.8.16** until Actions `publish-npm`
111
+ runs with `tag=v4.8.17` and `dry_run=false`).
112
+
113
+ ### Added
114
+ - A weaken, new layer, or new allow edge must name a short ADR /
115
+ decision-note path on `--policy-ack` (`adrPath`). A free-floating
116
+ reason is not enough
117
+ ([#264](https://github.com/pedroknigge/arkgate/pull/264)).
118
+
119
+ ### Fixed
120
+ - Retarget `policy-delta-ack-match` onto the current
121
+ `policyDeltaAcknowledgementMatches` body so the 90% confidence
122
+ floor still holds after #264
123
+ ([#265](https://github.com/pedroknigge/arkgate/pull/265)).
124
+
125
+ ## 4.8.16 — 2026-09-16
126
+
127
+ **Patch** over **4.8.15**. Daily accumulate of the five ships that landed
128
+ after 4.8.15 published: invariant tests path
129
+ ([#255](https://github.com/pedroknigge/arkgate/pull/255)), doctor green
130
+ cites ([#256](https://github.com/pedroknigge/arkgate/pull/256)),
131
+ coverageRoots when enforced
132
+ ([#257](https://github.com/pedroknigge/arkgate/pull/257)),
133
+ `start --apply` install honesty
134
+ ([#259](https://github.com/pedroknigge/arkgate/pull/259)), and prototype
135
+ shortcuts ([#260](https://github.com/pedroknigge/arkgate/pull/260)).
136
+ **Write. Check. Ship.** **No required config migration.** No
137
+ `schemaVersion` bump. Does not close `K01` / `Z09`. This mother
138
+ `ark.config.json` still does **not** turn `arkOrder` on.
139
+
140
+ **Status: prepared** (npm `latest` remains **4.8.15** until Actions `publish-npm`
141
+ runs with `tag=v4.8.16` and `dry_run=false`).
142
+
143
+ ### Added
144
+ - Adopted trees with domain invariants must name a real tests path, so
145
+ coverage cannot certify a test no runner runs
146
+ ([#255](https://github.com/pedroknigge/arkgate/pull/255)).
147
+ - Doctor green / healthy lines now cite a file, config key, or test — an
148
+ uncited line cannot look healthy
149
+ ([#256](https://github.com/pedroknigge/arkgate/pull/256)).
150
+ - When a domain invariant is enforced, `coverage.coverageRoots` is
151
+ required. Test globs alone are not enough
152
+ ([#257](https://github.com/pedroknigge/arkgate/pull/257)).
153
+ - Soft residual when SQLite, a JSON-file store, or admin literals stand
154
+ in for Persistence or Domain
155
+ ([#260](https://github.com/pedroknigge/arkgate/pull/260)).
156
+
157
+ ### Changed
158
+ - `start --apply` fails closed if the package install fails, and names
159
+ the exact command to recover
160
+ ([#259](https://github.com/pedroknigge/arkgate/pull/259)).
161
+ - Pack ceilings remeasured on the IT01 CI pack (run 34848276803).
162
+ ≥10% headroom. No new product surface.
163
+
99
164
  ## 4.8.15 — 2026-09-13
100
165
 
101
166
  **Patch** over **4.8.14**. Pre-adoption ship bar: opt-in `--local` /
package/README.md CHANGED
@@ -39,7 +39,7 @@ Works with Cursor, Claude, Codex, and Grok.
39
39
 
40
40
  </div>
41
41
 
42
- > **ArkGate 4.8.15** is prepared on this tree; npm `latest` remains **4.8.14** until `publish-npm` for `v4.8.15`.
42
+ > **ArkGate 4.8.17** is prepared on this tree; npm `latest` remains **4.8.16** until `publish-npm` for `v4.8.17`.
43
43
  > Write. Check. Ship. Adopted = required GitHub
44
44
  > status running `arkgate-check --strict-merge`, or an explicit `advisory-only` stance.
45
45
  > Status is compact (`arkgate-check --doctor`; `--all` for Details). Optional **ArkRun**
@@ -47,7 +47,7 @@ Works with Cursor, Claude, Codex, and Grok.
47
47
  > (`arkgate/order`) stops rewriting a big product choice — like the billing plan —
48
48
  > as if it were a seat count. Change those choices through a valve, not a generic update.
49
49
  > `@arkgate/runtime` is deprecated.
50
- > [4.8.15 prepared](CHANGELOG.md) · [4.8.11 published](docs/releases/4.8.11.md) · [4.8.10](docs/releases/4.8.10.md) · [4.8.9](docs/releases/4.8.9.md) · [4.8.8](docs/releases/4.8.8.md) · [4.8.7](docs/releases/4.8.7.md) · [4.8.6](docs/releases/4.8.6.md) · [4.8.5](docs/releases/4.8.5.md) · [4.8.4](docs/releases/4.8.4.md) · [4.8.3](docs/releases/4.8.3.md) · [4.8.2](docs/releases/4.8.2.md) · [4.8.1](docs/releases/4.8.1.md) · [4.8.0](docs/releases/4.8.0.md) · [Docs hub](docs/README.md) · [Voice](docs/product-voice.md)
50
+ > [4.8.17 prepared](CHANGELOG.md) · [4.8.11 published](docs/releases/4.8.11.md) · [4.8.10](docs/releases/4.8.10.md) · [4.8.9](docs/releases/4.8.9.md) · [4.8.8](docs/releases/4.8.8.md) · [4.8.7](docs/releases/4.8.7.md) · [4.8.6](docs/releases/4.8.6.md) · [4.8.5](docs/releases/4.8.5.md) · [4.8.4](docs/releases/4.8.4.md) · [4.8.3](docs/releases/4.8.3.md) · [4.8.2](docs/releases/4.8.2.md) · [4.8.1](docs/releases/4.8.1.md) · [4.8.0](docs/releases/4.8.0.md) · [Docs hub](docs/README.md) · [Voice](docs/product-voice.md)
51
51
 
52
52
  ---
53
53
 
@@ -69,10 +69,12 @@ Full map: **[docs/README.md](docs/README.md)**
69
69
  npm install -D arkgate typescript
70
70
  npx arkgate start # preview files + commands
71
71
  npx arkgate start --apply # compact config + host router + CI plan
72
- npx arkgate-check --doctor # status — one next step
73
- npx arkgate-check --doctor --all # full details
72
+ npx --package=arkgate arkgate-check --doctor # status — one next step
73
+ npx --package=arkgate arkgate-check --doctor --all # full details
74
74
  ```
75
75
 
76
+ `arkgate-check` is a command in the `arkgate` package, not its own npm package. After a local install, `npx arkgate-check --doctor` also works.
77
+
76
78
  `start --apply` refuses when projected governed coverage is below 50% or
77
79
  shape confidence is weak (below 0.6 with coverage under 80%). That lock is
78
80
  deliberate. Lock the shape with `--archetype <id>`, `--preset <name>`, or
@@ -342,7 +344,7 @@ Compact starters leave the extra off. Details: [ArkOrder](docs/arkorder.md).
342
344
  | Config · package surface · TS | [configuration](docs/configuration.md) · [package-surface](docs/package-surface.md) · [typescript-support](docs/typescript-support.md) |
343
345
  | Brownfield | [docs/brownfield-adoption.md](docs/brownfield-adoption.md) |
344
346
  | Security | [SECURITY.md](SECURITY.md) |
345
- | Prepared (4.8.15; not published) | [CHANGELOG](CHANGELOG.md) |
347
+ | Prepared (4.8.17; not published) | [CHANGELOG](CHANGELOG.md) |
346
348
  | Current published (4.8.11 on npm `latest`) | [docs/releases/4.8.11.md](docs/releases/4.8.11.md) · [CHANGELOG](CHANGELOG.md) |
347
349
  | Prior published (4.8.10) | [docs/releases/4.8.10.md](docs/releases/4.8.10.md) · [CHANGELOG](CHANGELOG.md) |
348
350
  | Prior published (4.8.9) | [docs/releases/4.8.9.md](docs/releases/4.8.9.md) · [CHANGELOG](CHANGELOG.md) |
@@ -1959,9 +1959,13 @@ async function main() {
1959
1959
  );
1960
1960
  console.error(` Next: ${finding.nextAction}`);
1961
1961
  }
1962
+ if (policyDelta.adrNote?.missing) {
1963
+ console.error(policyDelta.adrNote.ask);
1964
+ console.error(`Next: ${policyDelta.adrNote.nextAction}`);
1965
+ }
1962
1966
  console.error(
1963
1967
  `Policy transition blocked (${policyDelta.basePolicyHash} → ${policyDelta.candidatePolicyHash}). ` +
1964
- 'Provide --policy-ack with the exact hashes, finding ids, and a non-empty reason.'
1968
+ 'Provide --policy-ack with the exact hashes, finding ids, a non-empty reason, and adrPath to a short note under docs/adr/.'
1965
1969
  );
1966
1970
  }
1967
1971
  if (designCheck.failureText()) console.error(designCheck.failureText());
@@ -107,6 +107,7 @@ import {
107
107
  evaluateWriteDesignDelta,
108
108
  formatDesignDeltaBlock,
109
109
  } from './lib/design-delta.mjs';
110
+ import { attachPolicyAdrNote } from './lib/adr-path.mjs';
110
111
 
111
112
  const arkCheckBin = fileURLToPath(new URL('./ark-check.mjs', import.meta.url));
112
113
  const arkMcpLauncher = fileURLToPath(new URL('./ark-mcp.mjs', import.meta.url));
@@ -1995,7 +1996,8 @@ export async function runArkMcp({ hookInput } = {}) {
1995
1996
  'judgment-required, or weakening. Pass the previous baseConfig and optional ' +
1996
1997
  'candidateConfig (defaults to this project contract). Weakening and judgment-required ' +
1997
1998
  'results set isError unless acknowledgement exactly matches both policy hashes and all ' +
1998
- 'blocking finding ids. Read-only; never edits the contract.',
1999
+ 'blocking finding ids, and adrPath names a short note under docs/adr/ or docs/decisions/. ' +
2000
+ 'Read-only; never edits the contract.',
1999
2001
  inputSchema: {
2000
2002
  type: 'object',
2001
2003
  properties: {
@@ -2010,7 +2012,8 @@ export async function runArkMcp({ hookInput } = {}) {
2010
2012
  acknowledgement: {
2011
2013
  type: 'object',
2012
2014
  description:
2013
- 'Optional schemaVersion/basePolicyHash/candidatePolicyHash/findingIds/reason object.',
2015
+ 'Optional schemaVersion/basePolicyHash/candidatePolicyHash/findingIds/reason object. ' +
2016
+ 'When the transition needs an acknowledgement, adrPath must name a short note under docs/adr/ or docs/decisions/.',
2014
2017
  },
2015
2018
  },
2016
2019
  required: ['baseConfig'],
@@ -2468,11 +2471,15 @@ export async function runArkMcp({ hookInput } = {}) {
2468
2471
  };
2469
2472
  }
2470
2473
  try {
2471
- const result = ark.analyzePolicyDelta({
2472
- baseConfig,
2473
- candidateConfig: params?.arguments?.candidateConfig ?? config,
2474
- acknowledgement: params?.arguments?.acknowledgement,
2475
- });
2474
+ const acknowledgement = params?.arguments?.acknowledgement;
2475
+ const result = attachPolicyAdrNote(
2476
+ ark.analyzePolicyDelta({
2477
+ baseConfig,
2478
+ candidateConfig: params?.arguments?.candidateConfig ?? config,
2479
+ acknowledgement,
2480
+ }),
2481
+ { root: args.root, acknowledgement, failClosed: true }
2482
+ );
2476
2483
  return {
2477
2484
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
2478
2485
  structuredContent: result,
@@ -22,6 +22,7 @@ import {
22
22
  normalizeArkgateInstallSpec,
23
23
  packageInstallArgv,
24
24
  installDevHint,
25
+ arkPackageRecoveryCommand,
25
26
  } from './lib/package-manager.mjs';
26
27
 
27
28
  /**
@@ -674,6 +675,7 @@ export {
674
675
  normalizeArkgateInstallSpec,
675
676
  packageInstallArgv,
676
677
  installDevHint,
678
+ arkPackageRecoveryCommand,
677
679
  };
678
680
 
679
681
  // FX01–FX02: registry-aware skip lives in upgrade-package-decision (injectable probe).
package/bin/ark.mjs CHANGED
@@ -22,7 +22,12 @@ import {
22
22
  } from './ark-shared.mjs';
23
23
  import { pinArkgateDevDependency, FALSE_GREEN_GAP_ID } from './lib/field-install.mjs';
24
24
  import { validateHardWriteRequest } from './lib/enforcement-profiles.mjs';
25
- import { applyStartPreview, planStart, renderStartPreview } from './lib/start-preview.mjs';
25
+ import {
26
+ applyStartPreview,
27
+ formatStartPackageInstallFailure,
28
+ planStart,
29
+ renderStartPreview,
30
+ } from './lib/start-preview.mjs';
26
31
  import { runUpgradeCommand } from './lib/upgrade-command.mjs';
27
32
  import { detectActiveAgentHost } from './lib/skill-install.mjs';
28
33
  import { loadArkConfigContract } from './lib/config-contract.mjs';
@@ -489,7 +494,8 @@ async function start(args) {
489
494
  const skip = shouldSkipArkgateInstall(args.root, cliVersion());
490
495
  if (!skip.skip) {
491
496
  const [command, commandArgs] = packageInstallArgv(args.root, `^${cliVersion()}`);
492
- if (!args.json) console.log(`Installing package: ${command} ${commandArgs.join(' ')}`);
497
+ const installCommand = `${command} ${commandArgs.join(' ')}`;
498
+ if (!args.json) console.log(`Installing package: ${installCommand}`);
493
499
  // Keep stdout clean for --json consumers (package managers are chatty on stdout).
494
500
  const status = args.json
495
501
  ? (spawnSync(command, commandArgs, {
@@ -498,10 +504,9 @@ async function start(args) {
498
504
  encoding: 'utf8',
499
505
  }).status ?? 1)
500
506
  : runCommand(command, commandArgs, args.root);
501
- if (status !== 0 && !args.json) {
502
- console.log(
503
- `Package manager exited ${status}. package.json is pinned; run the install command when online.`
504
- );
507
+ if (status !== 0) {
508
+ console.error(formatStartPackageInstallFailure({ exitStatus: status, installCommand }));
509
+ return status;
505
510
  }
506
511
  }
507
512
  }
@@ -568,8 +573,12 @@ async function start(args) {
568
573
  if (pinned.changed) {
569
574
  console.log(` Pinned arkgate@${pinned.version} in package.json devDependencies.`);
570
575
  if (installStatus !== null && installStatus !== 0) {
571
- console.log(
572
- ` Package manager install exited ${installStatus} — package.json is still pinned; run install when online.`
576
+ const [command, commandArgs] = packageInstallArgv(root, pinned.version);
577
+ console.error(
578
+ formatStartPackageInstallFailure({
579
+ exitStatus: installStatus,
580
+ installCommand: `${command} ${commandArgs.join(' ')}`,
581
+ })
573
582
  );
574
583
  }
575
584
  } else if (pinned.reason === 'already-present') {
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Policy-ack ADR path tooth (AP02). Tooling I/O on the existing
3
+ * --policy-ack / --strict-merge plane. Doctor does not import this file.
4
+ */
5
+
6
+ import path from 'node:path';
7
+ import { ADR_PRESENCE_HOMES, isNonEmptyMarkdownFile } from './adr-presence.mjs';
8
+
9
+ export const ADR_PATH_ASK =
10
+ 'This change loosens a rule or adds a layer edge, but the acknowledgement has no decision-note path.';
11
+
12
+ export const ADR_PATH_NEXT =
13
+ 'Add a short note under docs/adr/ (or docs/decisions/) and put that file path in --policy-ack as adrPath.';
14
+
15
+ export const ADR_PATH_MISSING_FILE_ASK =
16
+ 'The acknowledgement names a decision note that is missing or empty.';
17
+
18
+ export const ADR_PATH_MISSING_FILE_NEXT =
19
+ 'Write that note (or fix adrPath) under docs/adr/ or docs/decisions/, then run ArkGate again.';
20
+
21
+ /**
22
+ * Relative path under a conventional decision-note home. No I/O.
23
+ *
24
+ * @param {unknown} value
25
+ * @returns {string | null}
26
+ */
27
+ export function canonicalizeAdrPath(value) {
28
+ if (typeof value !== 'string') return null;
29
+ const rel = value.trim().replace(/\\/g, '/');
30
+ if (!rel || rel.startsWith('/') || /^[A-Za-z]:\//.test(rel) || rel.includes('\0')) return null;
31
+ const parts = [];
32
+ for (const segment of rel.split('/')) {
33
+ if (!segment || segment === '.') continue;
34
+ if (segment === '..') return null;
35
+ parts.push(segment);
36
+ }
37
+ return parts.join('/') || null;
38
+ }
39
+
40
+ /**
41
+ * @param {unknown} value
42
+ * @returns {boolean}
43
+ */
44
+ export function isConventionalAdrPath(value) {
45
+ const rel = canonicalizeAdrPath(value);
46
+ if (!rel) return false;
47
+ for (const home of ADR_PRESENCE_HOMES) {
48
+ if (home.endsWith('.md')) {
49
+ if (rel === home) return true;
50
+ continue;
51
+ }
52
+ if (rel.startsWith(`${home}/`) && rel.toLowerCase().endsWith('.md') && rel.length > home.length + 4) {
53
+ return true;
54
+ }
55
+ }
56
+ return false;
57
+ }
58
+
59
+ /**
60
+ * Conventional path that already has a non-empty markdown file.
61
+ *
62
+ * @param {string} root
63
+ * @param {unknown} adrPath
64
+ * @returns {string | null}
65
+ */
66
+ export function resolveAdrNotePath(root, adrPath) {
67
+ const rel = isConventionalAdrPath(adrPath) ? canonicalizeAdrPath(adrPath) : null;
68
+ if (!rel || typeof root !== 'string' || root.length === 0) return null;
69
+ return isNonEmptyMarkdownFile(path.join(root, rel)) ? rel : null;
70
+ }
71
+
72
+ /**
73
+ * Residual when a weaken / new-edge needs a tied note path.
74
+ * `needed` is policyDelta.requiresAcknowledgement. Off → null.
75
+ *
76
+ * @param {{ root?: string, needed?: boolean, adrPath?: unknown }} [input]
77
+ * @returns {{ missing: true, ask: string, nextAction: string, path?: string } | null}
78
+ */
79
+ export function collectAdrPathResidual(input = {}) {
80
+ if (input.needed !== true) return null;
81
+ const rel = canonicalizeAdrPath(input.adrPath);
82
+ if (!rel) {
83
+ return { missing: true, ask: ADR_PATH_ASK, nextAction: ADR_PATH_NEXT };
84
+ }
85
+ if (resolveAdrNotePath(input.root ?? '', input.adrPath)) return null;
86
+ return {
87
+ missing: true,
88
+ path: rel,
89
+ ask: isConventionalAdrPath(input.adrPath) ? ADR_PATH_MISSING_FILE_ASK : ADR_PATH_ASK,
90
+ nextAction: isConventionalAdrPath(input.adrPath) ? ADR_PATH_MISSING_FILE_NEXT : ADR_PATH_NEXT,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Attach `adrNote` to a policy-delta result. Flips `valid` only when
96
+ * `failClosed` (existing policy-ack / --strict-merge plane) and the path
97
+ * is missing. Domain hash match stays I/O-free.
98
+ *
99
+ * @param {object | undefined} result
100
+ * @param {{ root?: string, acknowledgement?: { adrPath?: unknown }, failClosed?: boolean }} [input]
101
+ */
102
+ export function attachPolicyAdrNote(result, input = {}) {
103
+ if (!result || result.requiresAcknowledgement !== true) return result;
104
+ const residual = collectAdrPathResidual({
105
+ root: input.root,
106
+ needed: true,
107
+ adrPath: input.acknowledgement?.adrPath,
108
+ });
109
+ const adrNote = residual
110
+ ? residual
111
+ : { missing: false, path: canonicalizeAdrPath(input.acknowledgement?.adrPath) };
112
+ if (!residual || input.failClosed !== true) {
113
+ return { ...result, adrNote };
114
+ }
115
+ return { ...result, adrNote, valid: false };
116
+ }
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Soft ADR / decision-note presence when gates are demanded.
3
3
  * Tooling I/O. Never a gate fail. Absence is silent unless require-gates
4
- * or adopted-strict (required-merge) is on.
4
+ * or adopted-strict (required-merge) is on. The policy-ack path tooth lives
5
+ * in adr-path.mjs so doctor does not parse it.
5
6
  */
6
7
 
7
8
  import fs from 'node:fs';
@@ -23,7 +24,7 @@ export const ADR_PRESENCE_ASK =
23
24
  export const ADR_PRESENCE_NEXT =
24
25
  'Add a short note under docs/adr/ (or docs/decisions/) when you loosen a rule or add a real gate. Not every change.';
25
26
 
26
- function isNonEmptyMarkdownFile(file) {
27
+ export function isNonEmptyMarkdownFile(file) {
27
28
  if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return false;
28
29
  try {
29
30
  return fs.readFileSync(file, 'utf8').trim().length > 0;