arkgate 4.1.0 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,7 +5,51 @@ in the immutable pre-2.0 archive linked below.
5
5
 
6
6
  ## Unreleased
7
7
 
8
- ## 4.1.0 — 2026-07-25 (prepared)
8
+ ## 4.1.1 — 2026-07-25 (prepared)
9
+
10
+ **Patch** over 4.1.0. Phase EH — enforcement evidence modeling + documentation truth (field Codex
11
+ feedback + public-lane claims audit). Honesty and doctor/CI evidence taxonomy; first-push-safe CI
12
+ base-ref; report `.gitignore` coverage without defeating `!` exceptions. **No required config
13
+ migration.** Does not invent hard-write for Codex/Cursor/OpenCode. Does not close Z09 / RB-11.
14
+ **Status: prepared** (not on npm until publish).
15
+
16
+ ### Added
17
+
18
+ - **Doctor readiness split (EH05):** `productHonesty` separates contract/project readiness from
19
+ permanent soft-write host posture. Soft-write alone no longer forces global `Not finished`;
20
+ evidence keeps `soft-write-host` under `environmentResidualIds` with composite headline
21
+ (e.g. “Architecture contract ready; Codex local writes are advisory”) and an actionable
22
+ `primaryNextAction` pointing at the required GitHub status context.
23
+ - **Fields:** `contractReadiness`, `localWriteBoundary`, `architectureReasonIds`,
24
+ `environmentResidualIds` / `environmentResiduals` on `productHonesty` (additive JSON).
25
+ - **Provider policy reason (EH06):** GitHub Free/private branch-protection HTTP 403 →
26
+ `provider-policy-unavailable-plan` / `unavailable-plan` (not only generic
27
+ `provider-enforcement-unverified`).
28
+ - **CI runtime observation (EH06):** successful Actions runs can set `runtimeObserved: true`
29
+ independent of branch-protection API; `hard: false` when status is not required.
30
+ - **Repair envelope vs reinjection (EH07):** host matrix capabilities
31
+ `repair-envelope-emitted` / `repair-reinjection-guaranteed`; Codex emits envelope
32
+ (`--hook-repair`) without reinjection guarantee; minimum ops coverage
33
+ (`apply_patch` / `shell` / `pre-commit` → hard=false).
34
+ - **Claims matrix refresh (EH01):** `docs/audit/claims-matrix.md` dated 2026-07-25.
35
+
36
+ ### Fixed
37
+
38
+ - **Report `.gitignore` (EH03):** recognizes `.ark/*`, `/.ark/*`, `.ark/reports/` as coverage;
39
+ never appends broad `.ark/` after a narrower policy with `!` exceptions
40
+ (e.g. `!/.ark/golden-pattern.json`).
41
+ - **First-push CI base-ref (EH04):** generated workflow + Action clear all-zero
42
+ `github.event.before` and only pass `--fail-on-new-smells --base-ref` when
43
+ `git cat-file -e "${REF}^{commit}"` succeeds; otherwise full `--strict-merge` without delta.
44
+
45
+ ### Improved
46
+
47
+ - **CLI vs status context copy (EH07):** user-facing text distinguishes `arkgate-check` /
48
+ `ark-check` CLI from the GitHub **required status context** name.
49
+ - **Public lanes (EH02/EH08):** README host matrix, product-voice, ai-gates CI snippet, use/develop
50
+ honesty wording aligned with EH behavior; 0 Contradicted on public lanes.
51
+
52
+ ## 4.1.0 — 2026-07-25 (published)
9
53
 
10
54
  **Minor** over 4.0.1. Single ship train: field product phases + field-gap closure (S0–S7) +
11
55
  maintainer CI PR-slim / full-matrix speed work. Field product phases from Next monorepo dogfood +
@@ -15,7 +59,7 @@ write-root split, SPA/lib classification vacuum fixes, ESLint path-alias parity,
15
59
  false-positive tuning, ArkRules merge-plane honesty, type-edge placement debt, rules-inventory
16
60
  UI noise reduction, migrate-contract retrofit, and config write containment.
17
61
  **No required config migration.** Does not weaken write gate or CI value edges. Does not close
18
- Z09 / RB-11 field claims. **Status: prepared** (not on npm until publish).
62
+ Z09 / RB-11 field claims. **Status: published** (`arkgate@4.1.0` on npm `latest` at ship time).
19
63
 
20
64
  ### Added
21
65
 
package/README.md CHANGED
@@ -16,9 +16,8 @@ and makes sure a “green” check means something real.
16
16
 
17
17
  </div>
18
18
 
19
- > **ArkGate 4.0.1** is on npm `latest` (stale global CLI upgrade guard + ArkRules HTML/docs honesty).
20
- > **Next prepared:** [4.1.0](docs/releases/4.1.0.md) (field product train + CI PR slim — not published until registry).
21
- > [4.0.1 notes](docs/releases/4.0.1.md) · [Docs hub](docs/README.md) · [Product voice](docs/product-voice.md)
19
+ > **ArkGate 4.1.0** is on npm `latest`. Tree prepares **4.1.1** (Phase EH honesty + CI/report fixes) — [4.1.1 notes](docs/releases/4.1.1.md) (**prepared**, not on npm until publish).
20
+ > [4.1.0 notes](docs/releases/4.1.0.md) · [4.0.1](docs/releases/4.0.1.md) · [Docs hub](docs/README.md) · [Product voice](docs/product-voice.md)
22
21
 
23
22
  ---
24
23
 
@@ -99,17 +98,18 @@ Details: [docs/use.md](docs/use.md).
99
98
  <!-- arkgate-host-support:start -->
100
99
  | Host | Local write boundary | MCP validation | CI / merge path | Repair payload |
101
100
  |------|----------------------|----------------|-----------------|----------------|
102
- | Claude Code | **Hard** block for listed ops (PreToolUse `Write` / `Edit` / `MultiEdit`) when installed + trusted | Advisory; the agent must call it | **Required status** = hard merge boundary (`arkgate-check --strict-merge`) | Emitted on hook deny; host must re-inject |
103
- | Grok Build | **Hard** block for listed ops (PreToolUse `write` / `search_replace` (plus aliases)) when installed + trusted | Advisory; the agent must call it | **Required status** = hard merge boundary (`arkgate-check --strict-merge`) | Emitted on hook deny; host must re-inject |
104
- | Google Antigravity | **Hard** block for listed ops (PreToolUse `write_to_file` / `replace_file_content` / `multi_replace_file_content`) when installed + trusted | Advisory; the agent must call it | **Required status** = hard merge boundary (`arkgate-check --strict-merge`) | Emitted on hook deny; host must re-inject |
105
- | Cursor | **Advisory only** at write (no hard hook) | Advisory; the agent must call it | **Required status** = hard merge boundary (same CI) | No hard-boundary payload |
106
- | OpenAI Codex | **Advisory / best-effort** at write (not equivalent to Claude/Grok hard block) | Advisory; the agent must call it | **Required status** = hard merge boundary (same CI) | No hard-boundary payload |
107
- | OpenCode | **Advisory / best-effort** at write (MCP + optional plugin; not a hard boundary) | Advisory; the agent must call it | **Required status** = hard merge boundary (same CI) | No hard-boundary payload |
101
+ | Claude Code | **Hard** block for listed ops (PreToolUse `Write` / `Edit` / `MultiEdit`) when installed + trusted | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | Emitted on hook deny; host must re-inject (hard path when installed + trusted) |
102
+ | Grok Build | **Hard** block for listed ops (PreToolUse `write` / `search_replace` (plus aliases)) when installed + trusted | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | Emitted on hook deny; host must re-inject (hard path when installed + trusted) |
103
+ | Google Antigravity | **Hard** block for listed ops (PreToolUse `write_to_file` / `replace_file_content` / `multi_replace_file_content`) when installed + trusted | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | Emitted on hook deny; host must re-inject (hard path when installed + trusted) |
104
+ | Cursor | **Advisory only** at write (no hard hook) | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | No hard-boundary payload |
105
+ | OpenAI Codex | **Advisory / best-effort** at write (not equivalent to Claude/Grok hard block) | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | Envelope may emit (`--hook-repair`); reinjection **not** guaranteed (advisory host) |
106
+ | OpenCode | **Advisory / best-effort** at write (MCP + optional plugin; not a hard boundary) | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | No hard-boundary payload |
108
107
 
109
108
  **Read the CI column:** for every host, the repository-wide hard guarantee is a **required**
110
- merge check — not “CI file present.” Cursor/Codex/OpenCode never get a fake hard write claim.
109
+ GitHub **status context** that runs the CLI — not “CI file present,” and not the CLI binary name alone.
110
+ Cursor/Codex/OpenCode never get a fake hard write claim.
111
111
 
112
- This table describes the supported profile **after its files are installed and the host loads/trusts them**. A hard local boundary covers only the listed hook operations; alternate tools, direct filesystem writes, and human edits still rely on CI. MCP validation is advisory because the agent must call it. The CI check blocks a merge only when the repository makes that status required. Repair payloads never write code silently: the host must re-inject the candidate and ArkGate revalidates it. Run `arkgate-check --doctor` for the evidence actually detected in the current repository.
112
+ This table describes the supported profile **after its files are installed and the host loads/trusts them**. A hard local boundary covers only the listed hook operations; alternate tools, direct filesystem writes, and human edits still rely on CI. MCP validation is advisory because the agent must call it. The CI check blocks a merge only when the repository makes that status required. Repair **envelopes** may be emitted without reinjection being guaranteed; silent auto-apply never happens. Run `arkgate-check --doctor` (or `ark-check --doctor`) for the evidence actually detected in the current repository.
113
113
  <!-- arkgate-host-support:end -->
114
114
 
115
115
  #### Why the hard guarantee lives at the merge gate
@@ -186,8 +186,7 @@ for real systems. Details: [docs/production-hardening.md](docs/production-harden
186
186
  | Config · package surface · TS | [configuration](docs/configuration.md) · [package-surface](docs/package-surface.md) · [typescript-support](docs/typescript-support.md) |
187
187
  | Brownfield | [docs/brownfield-adoption.md](docs/brownfield-adoption.md) |
188
188
  | Security | [SECURITY.md](SECURITY.md) |
189
- | Current published (4.0.1) | [docs/releases/4.0.1.md](docs/releases/4.0.1.md) · [CHANGELOG](CHANGELOG.md) |
190
- | Next prepared (4.1.0) | [docs/releases/4.1.0.md](docs/releases/4.1.0.md) — not on npm until publish |
189
+ | Current published (4.1.0) | [docs/releases/4.1.0.md](docs/releases/4.1.0.md) · [CHANGELOG](CHANGELOG.md) |
191
190
  | Previous (4.0.0) | [docs/releases/4.0.0.md](docs/releases/4.0.0.md) |
192
191
  | Previous (3.9.2) | [docs/releases/3.9.2.md](docs/releases/3.9.2.md) |
193
192
  | History / maintainer evidence | [docs/archive/](docs/archive/README.md) |
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Ark report / local-state .gitignore coverage (EH03).
3
+ * Extracted from html-report.mjs to keep that module under its LOC budget.
4
+ */
5
+
6
+ /**
7
+ * Whether .gitignore already covers Ark local state / reports.
8
+ * Exact-line equality is insufficient: `.ark/*`, `/.ark/*`, and `.ark/reports/`
9
+ * (and common gitignore variants) already cover report output.
10
+ *
11
+ * @param {string} text
12
+ * @returns {boolean}
13
+ */
14
+ export function gitignoreCoversArkState(text) {
15
+ const lines = String(text || '')
16
+ .split(/\r?\n/)
17
+ .map((line) => line.trim())
18
+ .filter((line) => line.length > 0 && !line.startsWith('#'));
19
+
20
+ /** Patterns that cover the whole `.ark` tree or its report subtree. */
21
+ const cover = new Set([
22
+ '.ark',
23
+ '.ark/',
24
+ '/.ark',
25
+ '/.ark/',
26
+ '**/.ark',
27
+ '**/.ark/',
28
+ '.ark/*',
29
+ '/.ark/*',
30
+ '**/.ark/*',
31
+ '.ark/**',
32
+ '/.ark/**',
33
+ '**/.ark/**',
34
+ '.ark/reports',
35
+ '.ark/reports/',
36
+ '/.ark/reports',
37
+ '/.ark/reports/',
38
+ '.ark/reports/*',
39
+ '/.ark/reports/*',
40
+ '**/.ark/reports',
41
+ '**/.ark/reports/',
42
+ '**/.ark/reports/*',
43
+ '.ark/reports/**',
44
+ '/.ark/reports/**',
45
+ ]);
46
+
47
+ for (const line of lines) {
48
+ if (line.startsWith('!')) continue;
49
+ if (cover.has(line)) return true;
50
+ if (cover.has(`${line}/`) || cover.has(line.replace(/\/$/, ''))) return true;
51
+ }
52
+ return false;
53
+ }
54
+
55
+ /**
56
+ * Whether .gitignore already has a `!` exception under `.ark` (narrower policy).
57
+ * Appending a broad `.ark/` after such a policy defeats tracked golden-pattern exceptions.
58
+ *
59
+ * @param {string} text
60
+ * @returns {boolean}
61
+ */
62
+ export function gitignoreHasArkNegationException(text) {
63
+ return String(text || '')
64
+ .split(/\r?\n/)
65
+ .map((line) => line.trim())
66
+ .some((line) => line.startsWith('!') && /(^|\/)\.ark(\/|$|\b)/.test(line.slice(1)));
67
+ }
68
+
69
+ /**
70
+ * Decide whether report archiving should append a .gitignore rule (EH03).
71
+ * - Already covered → do not mutate.
72
+ * - Has `!` exceptions under `.ark` without full cover → append **narrow**
73
+ * `.ark/reports/` only (never broad `.ark/`, which would defeat exceptions).
74
+ * - Otherwise → append `.ark/`.
75
+ *
76
+ * @param {string} text
77
+ * @returns {{ append: boolean, rule: string|null, reason: string }}
78
+ */
79
+ export function arkGitignoreAppendDecision(text) {
80
+ if (gitignoreCoversArkState(text)) {
81
+ return { append: false, rule: null, reason: 'already-covered' };
82
+ }
83
+ if (gitignoreHasArkNegationException(text)) {
84
+ // Narrow reports-only ignore keeps !/.ark/golden-pattern.json (etc.) working.
85
+ return { append: true, rule: '.ark/reports/', reason: 'append-narrow-reports' };
86
+ }
87
+ return { append: true, rule: '.ark/', reason: 'append-broad' };
88
+ }
@@ -573,6 +573,22 @@ ${nodeSetup}
573
573
  ${qualityBlock ? `${qualityBlock}\n` : ''} - name: Ark architecture check
574
574
  env:
575
575
  ARK_POLICY_BASE_REF: \${{ github.event.pull_request.base.sha || github.event.before }}
576
- run: ${pm.run} --fail-on-new-smells --base-ref "\${{ github.event.pull_request.base.sha || github.event.before }}"
576
+ run: |
577
+ set -euo pipefail
578
+ # EH04: first push uses all-zero github.event.before — skip delta smells, keep full merge gate.
579
+ BASE_REF="\${ARK_POLICY_BASE_REF:-}"
580
+ if [[ "\$BASE_REF" =~ ^0{40,64}$ ]]; then
581
+ BASE_REF=""
582
+ fi
583
+ if [ -n "\$BASE_REF" ] && ! git cat-file -e "\${BASE_REF}^{commit}" 2>/dev/null; then
584
+ git fetch --no-tags --depth=1 origin "\$BASE_REF" 2>/dev/null || true
585
+ fi
586
+ if [ -n "\$BASE_REF" ] && git cat-file -e "\${BASE_REF}^{commit}" 2>/dev/null; then
587
+ export ARK_POLICY_BASE_REF="\$BASE_REF"
588
+ ${pm.run} --fail-on-new-smells --base-ref "\$BASE_REF"
589
+ else
590
+ export ARK_POLICY_BASE_REF=""
591
+ ${pm.run}
592
+ fi
577
593
  `;
578
594
  }
@@ -833,7 +833,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
833
833
  productHonesty.unfinished ? warn : ok,
834
834
  `${productHonesty.headline} — ${productHonesty.primaryMessage}`
835
835
  );
836
- if (productHonesty.unfinished && Array.isArray(productHonesty.reasonIds) && productHonesty.reasonIds.length > 0) {
836
+ if (Array.isArray(productHonesty.reasonIds) && productHonesty.reasonIds.length > 0) {
837
837
  line(' ', color.dim(`signals: ${productHonesty.reasonIds.join(', ')} (notAScore)`));
838
838
  }
839
839
  if (rulesUnderContract?.mergePlanes?.failMergeWhen) {
@@ -1045,9 +1045,17 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
1045
1045
  }
1046
1046
  const enforcement = writePath.enforcementState;
1047
1047
  for (const row of enforcementDoctorLines(enforcement)) line(row.level === 'ok' ? ok : row.level === 'bad' ? bad : warn, row.text);
1048
+ // EH07 Repair: use support/matrix caps (inventory omits envelope-emitted on Codex).
1049
+ const supportCaps = writePath.support?.capabilities || {};
1050
+ const repairReinjection = supportCaps['repair-reinjection-guaranteed'] === true;
1051
+ const repairEnvelope = supportCaps['repair-envelope-emitted'] === true || supportCaps['repair-payload'] === true;
1048
1052
  line(
1049
- capabilities['repair-payload'] ? ok : warn,
1050
- `Repair payload at hard boundary: ${capabilities['repair-payload'] ? 'yes' : 'no'}`
1053
+ repairReinjection ? ok : warn,
1054
+ repairReinjection
1055
+ ? 'Repair: envelope + reinjection guaranteed on hard path when installed + trusted'
1056
+ : repairEnvelope
1057
+ ? 'Repair: envelope may emit (`--hook-repair`); reinjection not guaranteed (advisory host)'
1058
+ : 'Repair: no hard-boundary payload'
1051
1059
  );
1052
1060
  if (writePath.gap) {
1053
1061
  line(writePath.gap.severity === 'warn' ? warn : warn, writePath.gap.message);
@@ -207,7 +207,8 @@ export function buildWritePathHonesty(activeHost, hardWriteActive = false, extra
207
207
  hardWriteSupported: hardCapable,
208
208
  hardWriteActive: effectiveHard,
209
209
  hardWriteUnverified: hardCapable && !effectiveHard,
210
- hardMergeBoundary: 'required-ci-status (arkgate-check --strict-merge)',
210
+ hardMergeBoundary:
211
+ 'required-github-status-context (CLI: arkgate-check --strict-merge / ark-check --strict-merge)',
211
212
  packageInstalled,
212
213
  packagePinAbsent: pinAbsentForUser,
213
214
  ...(pinCode ? { packagePinCode: pinCode } : {}),
@@ -367,10 +368,15 @@ export function buildProductHonesty(input = {}) {
367
368
  });
368
369
  }
369
370
 
371
+ // EH05: soft-write-host is a permanent host posture residual — keep in evidence,
372
+ // do NOT alone force architecture "Not finished". Reclassified out of contract debt.
370
373
  if (write?.softWriteHost) {
371
374
  reasons.push({
372
375
  id: 'soft-write-host',
373
- message: write.message || 'Local write is advisory; required CI status is the hard merge boundary.',
376
+ bucket: 'environment',
377
+ message:
378
+ write.message ||
379
+ 'Local write is advisory; hard merge boundary = a required GitHub status context running arkgate-check --strict-merge (alias ark-check --strict-merge).',
374
380
  });
375
381
  }
376
382
 
@@ -399,7 +405,13 @@ export function buildProductHonesty(input = {}) {
399
405
  // Informational only when no enforced arkrule plane — does not alone make unfinished.
400
406
  }
401
407
 
402
- const unfinished = reasons.length > 0;
408
+ // EH05: environment residual deny-list (future reason ids stay architecture debt by default).
409
+ const ENVIRONMENT_REASON_IDS = new Set(['soft-write-host']);
410
+
411
+ const environmentResiduals = reasons.filter((r) => ENVIRONMENT_REASON_IDS.has(r.id));
412
+ const architectureReasons = reasons.filter((r) => !ENVIRONMENT_REASON_IDS.has(r.id));
413
+ // unfinished = any non-environment residual (deny-list env, not allowlist architecture)
414
+ const unfinished = architectureReasons.length > 0;
403
415
  const wholeTreeGoverned = wholeTreeGovernedEarly;
404
416
  const coverageIncomplete =
405
417
  cov?.status === 'empty-scope' ||
@@ -407,31 +419,56 @@ export function buildProductHonesty(input = {}) {
407
419
  cov?.greenIsNotEnforcement === true ||
408
420
  !wholeTreeGoverned;
409
421
 
422
+ const softWriteOnly =
423
+ !unfinished && environmentResiduals.some((r) => r.id === 'soft-write-host');
424
+ const hostLabel = (() => {
425
+ const h = typeof write?.activeHost === 'string' ? write.activeHost.trim().toLowerCase() : '';
426
+ if (h === 'codex') return 'Codex';
427
+ if (h === 'cursor') return 'Cursor';
428
+ if (h === 'opencode') return 'OpenCode';
429
+ if (h) return h;
430
+ return 'this host';
431
+ })();
432
+
410
433
  const primary =
411
- reasons.find((r) => r.id === 'active-blocking-violations') ||
412
- reasons.find((r) => r.id === 'mode-adapt-with-debt') ||
413
- reasons.find((r) => r.id === 'mode-suggest-with-debt') ||
414
- reasons.find((r) => r.id === 'design-weak') ||
415
- reasons.find((r) => r.id === 'design-smells-open-edges') ||
416
- reasons.find((r) => r.id === 'coverage-weak-or-empty') ||
417
- reasons.find((r) => r.id === 'dirty-freeze') ||
418
- reasons.find((r) => r.id === 'package-version-dual-truth') ||
419
- reasons.find((r) => r.id === 'package-pin-absent') ||
420
- reasons.find((r) => r.id === 'baseline-missing-with-debt') ||
421
- reasons.find((r) => r.id === 'residual-pilot') ||
422
- reasons[0];
434
+ architectureReasons.find((r) => r.id === 'active-blocking-violations') ||
435
+ architectureReasons.find((r) => r.id === 'mode-adapt-with-debt') ||
436
+ architectureReasons.find((r) => r.id === 'mode-suggest-with-debt') ||
437
+ architectureReasons.find((r) => r.id === 'design-weak') ||
438
+ architectureReasons.find((r) => r.id === 'design-smells-open-edges') ||
439
+ architectureReasons.find((r) => r.id === 'coverage-weak-or-empty') ||
440
+ architectureReasons.find((r) => r.id === 'dirty-freeze') ||
441
+ architectureReasons.find((r) => r.id === 'package-version-dual-truth') ||
442
+ architectureReasons.find((r) => r.id === 'package-pin-absent') ||
443
+ architectureReasons.find((r) => r.id === 'baseline-missing-with-debt') ||
444
+ architectureReasons.find((r) => r.id === 'residual-pilot') ||
445
+ architectureReasons[0] ||
446
+ environmentResiduals[0];
423
447
 
424
- const primaryMessage = unfinished
425
- ? primary?.message ||
426
- 'Not finished: residual honesty signals remain (violations, mode, coverage, freeze, design, package pin, or pilots).'
427
- : wholeTreeGoverned
428
- ? 'No residual honesty blockers on this slice still not a numeric architecture score; re-doctor after material change.'
429
- : 'No residual honesty blockers flagged — green is only as wide as the governed slice.';
448
+ let primaryMessage;
449
+ if (unfinished) {
450
+ primaryMessage =
451
+ primary?.message ||
452
+ 'Not finished: residual honesty signals remain (violations, mode, coverage, freeze, design, package pin, or pilots).';
453
+ } else if (softWriteOnly) {
454
+ primaryMessage = `${hostLabel} local writes stay advisory/bypassable; architecture contract on this slice is ready. Hard merge boundary is a required GitHub status context running arkgate-check --strict-merge (alias ark-check --strict-merge).`;
455
+ } else if (wholeTreeGoverned) {
456
+ primaryMessage =
457
+ 'No residual honesty blockers on this slice — still not a numeric architecture score; re-doctor after material change.';
458
+ } else {
459
+ primaryMessage =
460
+ 'No residual honesty blockers flagged — green is only as wide as the governed slice.';
461
+ }
430
462
 
431
463
  // P0B-HEADLINE: dual-truth / pin-only unfinished must not claim "not whole-tree"
432
464
  // when the governed tree is already 100%.
465
+ // EH05: soft-write alone → composite readiness headline, never global "Not finished".
433
466
  let headline;
434
- if (!unfinished) {
467
+ if (!unfinished && softWriteOnly) {
468
+ headline = wholeTreeGoverned
469
+ ? `Architecture contract ready; ${hostLabel} local writes are advisory`
470
+ : `Contract residual clear; ${hostLabel} local writes are advisory`;
471
+ } else if (!unfinished) {
435
472
  headline = 'Honesty clear on residual signals';
436
473
  } else if (coverageIncomplete) {
437
474
  headline = 'Not finished / not whole-tree guarantee';
@@ -440,6 +477,7 @@ export function buildProductHonesty(input = {}) {
440
477
  }
441
478
 
442
479
  // Prefer caller next action; dual-truth / pin-absent get install/pin path when empty.
480
+ // Soft-write-only must not leave a failure headline with null next action (EH05).
443
481
  let primaryNextAction = input.primaryNextAction || null;
444
482
  if (!primaryNextAction && dualTruth) {
445
483
  const ver = input.packageVersionTruth?.cliVersion;
@@ -449,8 +487,20 @@ export function buildProductHonesty(input = {}) {
449
487
  } else if (!primaryNextAction && pinAbsent) {
450
488
  primaryNextAction =
451
489
  'Add arkgate to package.json and install so CI/npx resolve this CLI (PACKAGE_PIN_ABSENT)';
490
+ } else if (!primaryNextAction && softWriteOnly) {
491
+ primaryNextAction =
492
+ 'Confirm the GitHub required status context name runs arkgate-check --strict-merge (or ark-check --strict-merge). Soft-write hosts stay advisory at local write; the required status is the hard merge boundary.';
452
493
  }
453
494
 
495
+ const contractReadiness = unfinished ? 'not-ready' : wholeTreeGoverned ? 'ready' : 'partial';
496
+ const localWriteBoundary = write?.softWriteHost
497
+ ? 'advisory'
498
+ : write?.hardWriteActive
499
+ ? 'hard'
500
+ : write?.hardWriteSupported
501
+ ? 'unverified'
502
+ : 'unknown';
503
+
454
504
  return {
455
505
  finished: !unfinished && wholeTreeGoverned && !designWeak && activeBlocking === 0,
456
506
  elegant: !designWeak && !base?.dirtyBaselineRisk && !designSmellsOpenEdges && activeBlocking === 0,
@@ -462,8 +512,14 @@ export function buildProductHonesty(input = {}) {
462
512
  activeBlocking === 0,
463
513
  unfinished,
464
514
  notAScore: true,
515
+ // Full evidence including soft-write-host (reclassified, not silenced)
465
516
  reasonIds: reasons.map((r) => r.id),
466
517
  reasons,
518
+ architectureReasonIds: architectureReasons.map((r) => r.id),
519
+ environmentResidualIds: environmentResiduals.map((r) => r.id),
520
+ environmentResiduals,
521
+ contractReadiness,
522
+ localWriteBoundary,
467
523
  primaryMessage,
468
524
  primaryNextAction,
469
525
  headline,
@@ -1,2 +1,2 @@
1
1
  // Generated from enforcement-state.source.mjs — run npm run generate:packaged-tooling.
2
- import b from"node:fs";import{createRequire as q}from"node:module";import h from"node:path";const n="unverified";function S(a){const e=h.join(a,"package.json");try{if(JSON.parse(b.readFileSync(e,"utf8"))?.name==="arkgate"&&b.statSync(h.join(a,"bin","ark-check.mjs"),{throwIfNoEntry:!1})?.isFile())return{installed:!0,source:"package.json + bin/ark-check.mjs (self-host)",selfHost:!0}}catch{}try{const r=q(e).resolve("arkgate/package.json"),o=h.dirname(r),t=JSON.parse(b.readFileSync(r,"utf8")),l=h.join(o,"bin","ark-check.mjs");if(t?.name==="arkgate"&&b.statSync(l,{throwIfNoEntry:!1})?.isFile())return{installed:!0,source:"arkgate/package.json via project resolver",selfHost:!1}}catch{}return{installed:!1,source:"arkgate/package.json unresolved from project",selfHost:!1}}function M(a){return a.length>0?a:["filesystem scan (no matching configuration)"]}function y({supported:a,configuredPaths:e,installed:r,active:o,runtimeObserved:t,operation:l,operationCoverage:d,bypassable:u,required:c,hard:i,sources:s}){const p=e.length>0,v=!!r.installed;return{supported:a,analyzed:!0,configured:p,installed:v,active:o,runtimeObserved:t,operation:l,operationCoverage:d,bypassable:u,required:c,hard:i,evidence:[...M(e).map(g=>({field:"configured",source:g,value:p})),{field:"installed",source:r.source,value:v},{field:"active",source:s.active,value:o},{field:"runtimeObserved",source:s.runtimeObserved,value:t},{field:"operationCoverage",source:s.operationCoverage,value:d},{field:"bypassable",source:s.bypassable,value:u},{field:"required",source:s.required,value:c},{field:"hard",source:s.hard,value:i}]}}function O(a,e){const r=S(a),o=!!e.support?.capabilities?.["hard-write"],t=!!e.support?.capabilities?.["advisory-write"],l=e.capabilityEvidence["hard-write"],d=e.capabilityEvidence["advisory-write"],u=e.capabilityEvidence["merge-gate"],c=e.enforcementLadder.localWrite,i=typeof c.operationCovered=="boolean",s=i?c.operationCovered:n,p=i&&s===!0,v=!!(o&&r.installed&&p&&c.hard===!0),g=i?p&&r.installed:o&&l.length>0&&r.installed?n:!1,k=t&&d.length>0&&r.installed?n:!1,f=!!(e.ci?.failClosed&&u.length>0),C=f&&r.installed?n:!1;return{schemaVersion:"1.1",activeHost:e.activeHost,localWrite:y({supported:o,configuredPaths:l,installed:r,active:g,runtimeObserved:i,operation:i?c.operation??null:null,operationCoverage:s,bypassable:v?!1:o&&!i?n:!0,required:n,hard:v,sources:{active:i?"observed PreToolUse attempt":"runtime observation unavailable",runtimeObserved:i?"fresh PreToolUse invocation":"runtime observation unavailable",operationCoverage:i?"active-host operation matcher":"operation not observed",bypassable:v?"observed hard write boundary":"host runtime bypass evidence unavailable",required:"local host policy unavailable",hard:v?"fresh covered active-host invocation":"hardness not proven for this invocation"}}),advisoryMcp:y({supported:t,configuredPaths:d,installed:r,active:k,runtimeObserved:!1,operation:null,operationCoverage:n,bypassable:!0,required:n,hard:!1,sources:{active:"MCP runtime observation unavailable",runtimeObserved:"doctor did not observe an MCP tool invocation",operationCoverage:"advisory MCP is caller-invoked",bypassable:"advisory MCP does not intercept every write",required:"local host policy unavailable",hard:"MCP presence is advisory and never proves a hard boundary"}}),ciMerge:y({supported:!0,configuredPaths:f?u:[],installed:r,active:C,runtimeObserved:!1,operation:"merge",operationCoverage:f?n:!1,bypassable:f?n:!0,required:n,hard:!1,sources:{active:"CI run and provider enforcement not observed",runtimeObserved:"provider evidence unavailable",operationCoverage:"required-status operation coverage unavailable",bypassable:"branch-protection evidence unavailable",required:"branch-protection evidence unavailable",hard:"merge hardness requires fresh provider evidence"}})}}function w(a,e,r,o){return{...a,...o,evidence:[...a.evidence.filter(t=>!e.includes(t.field)),...e.map(t=>({field:t,source:r,value:o[t]}))]}}function x(a,e){if(!e?.available)return a;const r=typeof e.arkCheckRequired=="boolean"?e.arkCheckRequired:n,o=!!(a.enforcementState.ciMerge.configured&&a.enforcementState.ciMerge.installed),t=r===!0?o:r===!1?!1:o?n:!1,l=t===!0?e.arkCheckSourceBound===!1?!0:n:r===!1?!0:o?n:!0,d=`GitHub branch protection (${e.repo??"repository"}:${e.branch??"default"})`,u=!0,c=r,i=t===!0&&l===!1&&c===!0,s=w(a.enforcementState.ciMerge,["active","runtimeObserved","operationCoverage","bypassable","required","hard"],d,{active:t,runtimeObserved:u,operationCoverage:c,bypassable:l,required:r,hard:i});return{...a,enforcementState:{...a.enforcementState,ciMerge:s},enforcementLadder:{...a.enforcementLadder,ciMerge:{...a.enforcementLadder.ciMerge,requiredStatus:r}}}}function m(a,e){const r=o=>o===!0?"yes":o===!1?"no":String(o);return`${a} \u2014 supported: ${r(e.supported)} \xB7 analyzed: ${r(e.analyzed)} \xB7 configured: ${r(e.configured)} \xB7 installed: ${r(e.installed)} \xB7 runtime observed: ${r(e.runtimeObserved)} \xB7 operation: ${e.operation??"none"} \xB7 operation covered: ${r(e.operationCoverage)} \xB7 active: ${r(e.active)} \xB7 bypassable: ${r(e.bypassable)} \xB7 required: ${r(e.required)} \xB7 hard: ${r(e.hard)}`}function P(a){const e=[{level:a.localWrite.active===!0?"ok":"warn",text:m("Local write",a.localWrite)},{level:"warn",text:m("Advisory MCP",a.advisoryMcp)},{level:a.ciMerge.required===!0?"ok":"warn",text:m("CI merge",a.ciMerge)}];return a.localWrite.active===n&&a.localWrite.hard===!1&&e.push({level:"bad",text:"RED FLAG: local hook assets exist, but this active-host operation was not observed at runtime; hard blocking is unverified."}),a.activeHost==="unknown"&&e.push({level:"warn",text:"Active host unknown for this invocation \u2014 enforcementState is session projection only. See writePath.inventory for on-disk host hooks; hard write is never claimed without runtime proof."}),e}export{O as buildEnforcementState,P as enforcementDoctorLines,S as packageInstallation,x as withCiProviderEvidence};
2
+ import m from"node:fs";import{createRequire as q}from"node:module";import h from"node:path";const n="unverified";function S(a){const e=h.join(a,"package.json");try{if(JSON.parse(m.readFileSync(e,"utf8"))?.name==="arkgate"&&m.statSync(h.join(a,"bin","ark-check.mjs"),{throwIfNoEntry:!1})?.isFile())return{installed:!0,source:"package.json + bin/ark-check.mjs (self-host)",selfHost:!0}}catch{}try{const r=q(e).resolve("arkgate/package.json"),o=h.dirname(r),i=JSON.parse(m.readFileSync(r,"utf8")),s=h.join(o,"bin","ark-check.mjs");if(i?.name==="arkgate"&&m.statSync(s,{throwIfNoEntry:!1})?.isFile())return{installed:!0,source:"arkgate/package.json via project resolver",selfHost:!1}}catch{}return{installed:!1,source:"arkgate/package.json unresolved from project",selfHost:!1}}function M(a){return a.length>0?a:["filesystem scan (no matching configuration)"]}function g({supported:a,configuredPaths:e,installed:r,active:o,runtimeObserved:i,operation:s,operationCoverage:v,bypassable:u,required:c,hard:t,sources:l}){const p=e.length>0,d=!!r.installed;return{supported:a,analyzed:!0,configured:p,installed:d,active:o,runtimeObserved:i,operation:s,operationCoverage:v,bypassable:u,required:c,hard:t,evidence:[...M(e).map(f=>({field:"configured",source:f,value:p})),{field:"installed",source:r.source,value:d},{field:"active",source:l.active,value:o},{field:"runtimeObserved",source:l.runtimeObserved,value:i},{field:"operationCoverage",source:l.operationCoverage,value:v},{field:"bypassable",source:l.bypassable,value:u},{field:"required",source:l.required,value:c},{field:"hard",source:l.hard,value:t}]}}function O(a,e){const r=S(a),o=!!e.support?.capabilities?.["hard-write"],i=!!e.support?.capabilities?.["advisory-write"],s=e.capabilityEvidence["hard-write"],v=e.capabilityEvidence["advisory-write"],u=e.capabilityEvidence["merge-gate"],c=e.enforcementLadder.localWrite,t=typeof c.operationCovered=="boolean",l=t?c.operationCovered:n,p=t&&l===!0,d=!!(o&&r.installed&&p&&c.hard===!0),f=t?p&&r.installed:o&&s.length>0&&r.installed?n:!1,b=i&&v.length>0&&r.installed?n:!1,y=!!(e.ci?.failClosed&&u.length>0),C=y&&r.installed?n:!1;return{schemaVersion:"1.1",activeHost:e.activeHost,localWrite:g({supported:o,configuredPaths:s,installed:r,active:f,runtimeObserved:t,operation:t?c.operation??null:null,operationCoverage:l,bypassable:d?!1:o&&!t?n:!0,required:n,hard:d,sources:{active:t?"observed PreToolUse attempt":"runtime observation unavailable",runtimeObserved:t?"fresh PreToolUse invocation":"runtime observation unavailable",operationCoverage:t?"active-host operation matcher":"operation not observed",bypassable:d?"observed hard write boundary":"host runtime bypass evidence unavailable",required:"local host policy unavailable",hard:d?"fresh covered active-host invocation":"hardness not proven for this invocation"}}),advisoryMcp:g({supported:i,configuredPaths:v,installed:r,active:b,runtimeObserved:!1,operation:null,operationCoverage:n,bypassable:!0,required:n,hard:!1,sources:{active:"MCP runtime observation unavailable",runtimeObserved:"doctor did not observe an MCP tool invocation",operationCoverage:"advisory MCP is caller-invoked",bypassable:"advisory MCP does not intercept every write",required:"local host policy unavailable",hard:"MCP presence is advisory and never proves a hard boundary"}}),ciMerge:g({supported:!0,configuredPaths:y?u:[],installed:r,active:C,runtimeObserved:!1,operation:"merge",operationCoverage:y?n:!1,bypassable:y?n:!0,required:n,hard:!1,sources:{active:"CI run and provider enforcement not observed",runtimeObserved:"provider evidence unavailable",operationCoverage:"required-status operation coverage unavailable",bypassable:"branch-protection evidence unavailable",required:"branch-protection evidence unavailable",hard:"merge hardness requires fresh provider evidence"}})}}function $(a,e,r,o){return{...a,...o,evidence:[...a.evidence.filter(i=>!e.includes(i.field)),...e.map(i=>({field:i,source:r,value:o[i]}))]}}function R(a,e){if(!e)return a;const r=e.reason==="provider-policy-unavailable-plan"||e.policyReason==="unavailable-plan",o=e.available===!0,i=!o&&!r&&(e.reason==="provider-enforcement-unverified"||e.reason==="gh-cli-unavailable"||e.reason==="gh-repo-unavailable"||!!e.reason);if(!o&&e.runtimeObserved!==!0&&!r&&!i)return a;const s=o?typeof e.arkCheckRequired=="boolean"?e.arkCheckRequired:n:r?!1:n,v=!!(a.enforcementState.ciMerge.configured&&a.enforcementState.ciMerge.installed),u=s===!0?v:s===!1?!1:v?n:!1,c=u===!0?e.arkCheckSourceBound===!1?!0:n:s===!1?!0:v?n:!0,t=e.runtimeObserved===!0,l=o?`GitHub branch protection (${e.repo??"repository"}:${e.branch??"default"})`:r?`GitHub provider policy unavailable (plan) (${e.repo??"repository"}:${e.branch??"default"})`:`GitHub CI runtime (${e.repo??"repository"})`,p=s,d=u===!0&&c===!1&&p===!0,f=$(a.enforcementState.ciMerge,["active","runtimeObserved","operationCoverage","bypassable","required","hard"],l,{active:u,runtimeObserved:t,operationCoverage:p,bypassable:c,required:s,hard:d}),b={...a,enforcementState:{...a.enforcementState,ciMerge:f},enforcementLadder:{...a.enforcementLadder,ciMerge:{...a.enforcementLadder.ciMerge,requiredStatus:s,...e.latestCiRun?{latestCiRun:e.latestCiRun}:{},...r?{providerPolicy:"unavailable-plan"}:{}}}};return(r||e.reason)&&(b.providerEnforcement={available:o,reason:e.reason||(r?"provider-policy-unavailable-plan":"provider-enforcement-unverified"),policyReason:e.policyReason||(r?"unavailable-plan":null),runtimeObserved:t,latestCiRun:e.latestCiRun??null,hard:d===!0}),b}function k(a,e){const r=o=>o===!0?"yes":o===!1?"no":String(o);return`${a} \u2014 supported: ${r(e.supported)} \xB7 analyzed: ${r(e.analyzed)} \xB7 configured: ${r(e.configured)} \xB7 installed: ${r(e.installed)} \xB7 runtime observed: ${r(e.runtimeObserved)} \xB7 operation: ${e.operation??"none"} \xB7 operation covered: ${r(e.operationCoverage)} \xB7 active: ${r(e.active)} \xB7 bypassable: ${r(e.bypassable)} \xB7 required: ${r(e.required)} \xB7 hard: ${r(e.hard)}`}function x(a){const e=[{level:a.localWrite.active===!0?"ok":"warn",text:k("Local write",a.localWrite)},{level:"warn",text:k("Advisory MCP",a.advisoryMcp)},{level:a.ciMerge.required===!0?"ok":"warn",text:k("CI merge",a.ciMerge)}];return a.localWrite.active===n&&a.localWrite.hard===!1&&e.push({level:"bad",text:"RED FLAG: local hook assets exist, but this active-host operation was not observed at runtime; hard blocking is unverified."}),a.activeHost==="unknown"&&e.push({level:"warn",text:"Active host unknown for this invocation \u2014 enforcementState is session projection only. See writePath.inventory for on-disk host hooks; hard write is never claimed without runtime proof."}),e}export{O as buildEnforcementState,x as enforcementDoctorLines,S as packageInstallation,R as withCiProviderEvidence};