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 +46 -2
- package/README.md +12 -13
- package/bin/lib/ark-gitignore.mjs +88 -0
- package/bin/lib/ci-and-commands.mjs +17 -1
- package/bin/lib/doctor-plan.mjs +11 -3
- package/bin/lib/enforcement-honesty.mjs +78 -22
- package/bin/lib/enforcement-state.mjs +1 -1
- package/bin/lib/github-enforcement.mjs +152 -4
- package/bin/lib/host-support-matrix.mjs +91 -17
- package/bin/lib/html-report-depth.mjs +13 -2
- package/bin/lib/html-report.mjs +7 -8
- package/bin/lib/write-path-detect.mjs +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/docs/README.md +4 -4
- package/docs/agent-guide.md +5 -3
- package/docs/ai-gates.md +30 -7
- package/docs/develop.md +16 -6
- package/docs/package-surface.md +5 -3
- package/docs/product-voice.md +9 -3
- package/docs/use.md +3 -1
- package/package.json +1 -1
- package/server.json +2 -2
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.
|
|
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:
|
|
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
|
|
20
|
-
>
|
|
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**
|
|
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**
|
|
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**
|
|
105
|
-
| Cursor | **Advisory only** at write (no hard hook) | Advisory; the agent must call it | **Required status**
|
|
106
|
-
| OpenAI Codex | **Advisory / best-effort** at write (not equivalent to Claude/Grok hard block) | Advisory; the agent must call it | **Required status**
|
|
107
|
-
| OpenCode | **Advisory / best-effort** at write (MCP + optional plugin; not a hard boundary) | Advisory; the agent must call it | **Required status**
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
}
|
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
1050
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
|
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};
|