arkgate 3.9.0 → 3.9.2
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 +58 -0
- package/README.md +5 -4
- package/bin/lib/ambient-state.mjs +64 -8
- package/bin/lib/doctor-advisories.mjs +8 -4
- package/bin/lib/doctor-plan.mjs +47 -13
- package/bin/lib/enforcement-honesty.mjs +225 -0
- package/bin/lib/graph-blind.mjs +235 -0
- package/bin/lib/html-report-advisories.mjs +5 -3
- package/bin/lib/pilot-loop.mjs +19 -0
- package/bin/lib/post-green-path.mjs +22 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/docs/README.md +2 -2
- package/docs/package-surface.md +2 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/templates/skills/ark-coverage.md +7 -0
- package/templates/skills/ark-explore.md +4 -1
- package/templates/skills/ark-place.md +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,64 @@ in the immutable pre-2.0 archive linked below.
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 3.9.2 — 2026-07-23
|
|
9
|
+
|
|
10
|
+
**Patch** over 3.9.1. Product honesty for post-validity coaching, coverage/host write paths, and
|
|
11
|
+
advisory analysis precision — **no required config migration**, no gate weakening, no hard-write
|
|
12
|
+
claims on soft hosts. Y07 / Y09 remain **parked** (not promoted).
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Enforcement honesty helpers** (`bin/lib/enforcement-honesty.mjs`): coverage honesty
|
|
17
|
+
(empty / weak <50% worse-than-no-gate / partial / strong; `greenIsNotEnforcement` until 100%;
|
|
18
|
+
`wholeTreeGoverned`), baseline dirty-freeze risk, write-path honesty with soft hosts derived from
|
|
19
|
+
`HOST_SUPPORT_MATRIX` (fail-closed: soft never `hardWriteActive`).
|
|
20
|
+
- **Graph blind spots** (`bin/lib/graph-blind.mjs`): advisory scan for unresolvable dynamic
|
|
21
|
+
import/require edges (template-interpolation + non-literals + import-equals). Never a hard
|
|
22
|
+
architecture verdict; Y09 direction only.
|
|
23
|
+
- Doctor / plan JSON: `coverageHonesty`, `baseline.honesty`, `writePath.honesty`,
|
|
24
|
+
`graphBlindSpots`, design-weak honesty flags (`healthyFinishedForbidden`,
|
|
25
|
+
`multiPilotBatchForbidden`, `autoApplyForbidden` / `autoApplyPlanBForbidden`).
|
|
26
|
+
- Focused unit suite `tests/unit/static-check/enforcementHonesty.test.ts`.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **Post-green path:** placement coaching + shared design-weak honesty flags.
|
|
31
|
+
- **Pilot loop:** one-at-a-time queue (`queuedBets` / `queueNote`); multi-pilot batch and silent
|
|
32
|
+
plan-B auto-apply forbidden on all return paths.
|
|
33
|
+
- **Ambient sensor (Y07 honesty only):** status vocabulary (`idle` / `active-clean` /
|
|
34
|
+
`active-findings` / `unavailable`), `blockerGrade: false`, `strictDiagnostics: 'parked-Y07'`;
|
|
35
|
+
idle/clean/unavailable print honesty lines (not silence-as-done).
|
|
36
|
+
- **Skills** (`ark-coverage`, `ark-explore`, `ark-place`): deepen honesty / one-pilot routing
|
|
37
|
+
without new skill basenames.
|
|
38
|
+
- **HTML advisories:** graphBlindSpots X01 parity; ambient h2 parked-Y07 wording.
|
|
39
|
+
|
|
40
|
+
### Notes
|
|
41
|
+
|
|
42
|
+
- Soft write hosts (Cursor / Codex / OpenCode) remain advisory at write; required CI status is the
|
|
43
|
+
hard merge boundary.
|
|
44
|
+
- Z09 / residual `RB-11` remain open. Y07 / Y09 not marked done.
|
|
45
|
+
|
|
46
|
+
## 3.9.1 — 2026-07-23
|
|
47
|
+
|
|
48
|
+
**Patch** over 3.9.0. Repo hygiene and CI honesty only — **no required config migration**, no gate
|
|
49
|
+
weakening, no product API changes.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- **Onboarding matrix (`o04`):** `ark start --install` may rewrite package-manager lockfiles when the
|
|
54
|
+
published package is installed; tests compare product mutation paths only (lockfile drift allowed).
|
|
55
|
+
- **Docs post-3.9.0 publish:** CONTRIBUTING / README / release notes / migrate guide match npm truth
|
|
56
|
+
for the published line (updated again for this patch after 3.9.1 lands on `latest`).
|
|
57
|
+
- **Supply-chain hygiene:** `fast-uri` **3.1.4** (transitive via `ajv`) closes host-confusion advisory
|
|
58
|
+
GHSA-v2hh-gcrm-f6hx. Eval fixtures pin **Next.js 15.5.21** (patched) so Dependabot Next alerts on
|
|
59
|
+
`eval/cases/**` corpus close without shipping Next in the `arkgate` npm tarball.
|
|
60
|
+
|
|
61
|
+
### Notes
|
|
62
|
+
|
|
63
|
+
- Next bumps are **eval fixture hygiene**, not a consumer-runtime CVE in the published package.
|
|
64
|
+
- Z09 / residual `RB-11` remain open.
|
|
65
|
+
|
|
8
66
|
## 3.9.0 — 2026-07-23
|
|
9
67
|
|
|
10
68
|
**Beautiful Path** minor: one primary flow, doctor as control plane, progressive disclosure, and
|
package/README.md
CHANGED
|
@@ -16,9 +16,10 @@ and makes sure a “green” check means something real.
|
|
|
16
16
|
|
|
17
17
|
</div>
|
|
18
18
|
|
|
19
|
-
> **ArkGate 3.9.
|
|
20
|
-
> 3.
|
|
21
|
-
> [
|
|
19
|
+
> **ArkGate 3.9.2** is the next prepared patch (enforcement honesty: coverage/host write path,
|
|
20
|
+
> design-weak one-pilot coaching, advisory graph-blind). **npm `latest` is still 3.9.1** until
|
|
21
|
+
> publication succeeds. [3.9.2 notes](docs/releases/3.9.2.md) · [3.9.1](docs/releases/3.9.1.md) ·
|
|
22
|
+
> [Docs hub](docs/README.md)
|
|
22
23
|
|
|
23
24
|
---
|
|
24
25
|
|
|
@@ -175,7 +176,7 @@ for real systems. Details: [docs/production-hardening.md](docs/production-harden
|
|
|
175
176
|
| Config · package surface · TS | [configuration](docs/configuration.md) · [package-surface](docs/package-surface.md) · [typescript-support](docs/typescript-support.md) |
|
|
176
177
|
| Brownfield | [docs/brownfield-adoption.md](docs/brownfield-adoption.md) |
|
|
177
178
|
| Security | [SECURITY.md](SECURITY.md) |
|
|
178
|
-
| Latest release (3.9.
|
|
179
|
+
| Latest release (3.9.1 on npm; 3.9.2 prepared) | [3.9.2](docs/releases/3.9.2.md) · [3.9.1](docs/releases/3.9.1.md) · [CHANGELOG](CHANGELOG.md) |
|
|
179
180
|
| History / maintainer evidence | [docs/archive/](docs/archive/README.md) |
|
|
180
181
|
|
|
181
182
|
---
|
|
@@ -160,19 +160,37 @@ export function detectAmbientState(ts, root, config, files, ackState = { exists:
|
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Status vocabulary for ambient sensor honesty (Y07 strict stays parked).
|
|
165
|
+
* @param {{ active?: boolean, findingCount?: number }} result
|
|
166
|
+
*/
|
|
167
|
+
export function ambientSensorStatus(result) {
|
|
168
|
+
if (!result?.active) return 'idle';
|
|
169
|
+
return (result.findingCount ?? 0) > 0 ? 'active-findings' : 'active-clean';
|
|
170
|
+
}
|
|
171
|
+
|
|
163
172
|
/** JSON summary for doctor. Advisory only — never a verdict input. */
|
|
164
173
|
export function summarizeAmbientState(result, ackState = { exists: false, acks: [] }) {
|
|
174
|
+
const findingCount = result.findings.length;
|
|
175
|
+
const status = ambientSensorStatus({ active: result.active, findingCount });
|
|
165
176
|
return {
|
|
166
177
|
available: true,
|
|
167
178
|
active: result.active,
|
|
179
|
+
status,
|
|
168
180
|
advisory: true,
|
|
169
|
-
|
|
181
|
+
// Strict (blocker-grade) ambient diagnostics remain parked (Y07) until a real pure corpus.
|
|
182
|
+
blockerGrade: false,
|
|
183
|
+
strictDiagnostics: 'parked-Y07',
|
|
184
|
+
findingCount,
|
|
170
185
|
acknowledged: ackState?.invalid ? 0 : result.acknowledgedCount,
|
|
171
186
|
...(result.truncated > 0 ? { truncated: result.truncated } : {}),
|
|
172
187
|
...(result.skippedFiles > 0 ? { skippedFiles: result.skippedFiles } : {}),
|
|
173
|
-
note:
|
|
174
|
-
|
|
175
|
-
|
|
188
|
+
note:
|
|
189
|
+
status === 'idle'
|
|
190
|
+
? 'Idle: no pure: true layer opted in. Sensor stays advisory; blocker-grade ambient diagnostics are parked (Y07) until a real pure-layer field corpus exists. Opt in via layer pure: true when ready.'
|
|
191
|
+
: status === 'active-findings'
|
|
192
|
+
? 'Module-scope mutable state in pure layers — advisory only (never a hard verdict). Acknowledge deliberate registries in the sidecar or move state behind a port. Strict diagnostics remain parked (Y07).'
|
|
193
|
+
: 'Pure layers opted in; no module-scope let/var findings in the MVP envelope. Advisory sensor only — not a pass for blocker-grade ambient enforcement (Y07 parked).',
|
|
176
194
|
ackFile: {
|
|
177
195
|
path: ackState?.path ?? AMBIENT_STATE_ACKS_PATH,
|
|
178
196
|
present: ackState?.exists === true,
|
|
@@ -188,11 +206,14 @@ export function computeAmbientState(ts, root, config, files) {
|
|
|
188
206
|
return {
|
|
189
207
|
available: false,
|
|
190
208
|
active: false,
|
|
209
|
+
status: 'unavailable',
|
|
191
210
|
advisory: true,
|
|
211
|
+
blockerGrade: false,
|
|
212
|
+
strictDiagnostics: 'parked-Y07',
|
|
192
213
|
findings: [],
|
|
193
214
|
findingCount: 0,
|
|
194
215
|
acknowledged: 0,
|
|
195
|
-
note: 'TypeScript was not available to the doctor run; the ambient-state sensor did not execute.',
|
|
216
|
+
note: 'TypeScript was not available to the doctor run; the ambient-state sensor did not execute. Advisory only; Y07 strict remains parked.',
|
|
196
217
|
};
|
|
197
218
|
}
|
|
198
219
|
const ackState = loadAmbientStateAcks(root);
|
|
@@ -200,9 +221,41 @@ export function computeAmbientState(ts, root, config, files) {
|
|
|
200
221
|
return { ...summarizeAmbientState(result, ackState), findings: result.findings };
|
|
201
222
|
}
|
|
202
223
|
|
|
203
|
-
/**
|
|
224
|
+
/**
|
|
225
|
+
* Human doctor section (advisory).
|
|
226
|
+
* Idle prints a single dim honesty line so silence is not misread as "ambient done."
|
|
227
|
+
*/
|
|
204
228
|
export function printAmbientStateSection(state, io) {
|
|
205
|
-
if (!state
|
|
229
|
+
if (!state) return;
|
|
230
|
+
if (!state.available) {
|
|
231
|
+
console.log('');
|
|
232
|
+
console.log(io.color.bold('Ambient state (advisory)'));
|
|
233
|
+
io.line(' ', io.color.dim(state.note || 'Ambient sensor unavailable.'));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (state.status === 'idle' && !state.ackFile?.invalid) {
|
|
237
|
+
console.log('');
|
|
238
|
+
console.log(io.color.bold('Ambient state (advisory)'));
|
|
239
|
+
io.line(
|
|
240
|
+
' ',
|
|
241
|
+
io.color.dim(
|
|
242
|
+
'Idle (no pure: true layer) — advisory sensor only; blocker-grade ambient diagnostics parked (Y07).'
|
|
243
|
+
)
|
|
244
|
+
);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (state.status === 'active-clean' && !state.ackFile?.invalid && !state.findingCount) {
|
|
248
|
+
console.log('');
|
|
249
|
+
console.log(io.color.bold('Ambient state (advisory)'));
|
|
250
|
+
io.line(
|
|
251
|
+
' ',
|
|
252
|
+
io.color.dim(
|
|
253
|
+
'Pure layers clean under MVP envelope — still advisory; not Y07 blocker-grade pass.'
|
|
254
|
+
)
|
|
255
|
+
);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (!state.findingCount && !state.ackFile?.invalid) return;
|
|
206
259
|
console.log('');
|
|
207
260
|
console.log(io.color.bold('Ambient state (advisory)'));
|
|
208
261
|
if (state.ackFile?.invalid) {
|
|
@@ -217,5 +270,8 @@ export function printAmbientStateSection(state, io) {
|
|
|
217
270
|
if (state.acknowledged > 0) {
|
|
218
271
|
io.line(' ', io.color.dim(`acknowledged module state: ${state.acknowledged}`));
|
|
219
272
|
}
|
|
220
|
-
io.line(
|
|
273
|
+
io.line(
|
|
274
|
+
' ',
|
|
275
|
+
io.color.dim('advisory only — never blocks; move state behind a port or acknowledge it (Y07 strict parked)')
|
|
276
|
+
);
|
|
221
277
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Doctor's advisory sensors, aggregated (W01 contract health, U05 ambient
|
|
3
|
-
* state, X04 physical cohesion, Y03 parse health
|
|
4
|
-
* architecture violations or designFitness findings;
|
|
5
|
-
* parse-health evidence to analysis completeness and
|
|
6
|
-
* seam keeps doctor-plan.mjs inside its module budget
|
|
3
|
+
* state, X04 physical cohesion, Y03 parse health, graph-blind template edges).
|
|
4
|
+
* These sensors do not create architecture violations or designFitness findings;
|
|
5
|
+
* Z02 separately maps parse-health evidence to analysis completeness and
|
|
6
|
+
* fail-closed exits. One seam keeps doctor-plan.mjs inside its module budget.
|
|
7
7
|
*/
|
|
8
8
|
import { computeAmbientState, printAmbientStateSection } from './ambient-state.mjs';
|
|
9
9
|
import { computeContractHealth, printContractHealthSection } from './contract-smells.mjs';
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
printReshapeDecisionsSection,
|
|
18
18
|
} from './reshape-decisions.mjs';
|
|
19
19
|
import { printParseHealthSection, summarizeParseHealth } from './parse-health.mjs';
|
|
20
|
+
import { detectGraphBlindSpots, printGraphBlindSection } from './graph-blind.mjs';
|
|
20
21
|
|
|
21
22
|
export function computeDoctorAdvisories(root, config, cov, rules, files, ts, parseHealth) {
|
|
22
23
|
const physicalCohesion = computePhysicalCohesion(root, files);
|
|
@@ -33,6 +34,8 @@ export function computeDoctorAdvisories(root, config, cov, rules, files, ts, par
|
|
|
33
34
|
ambientState: computeAmbientState(ts, root, config, files),
|
|
34
35
|
physicalCohesion,
|
|
35
36
|
parseHealth: parseHealth ?? summarizeParseHealth(),
|
|
37
|
+
// Y09 direction: advisory graph-blind spots (template-interpolation); never hard verdict.
|
|
38
|
+
graphBlindSpots: detectGraphBlindSpots(ts, root, files),
|
|
36
39
|
};
|
|
37
40
|
}
|
|
38
41
|
|
|
@@ -46,4 +49,5 @@ export function printDoctorAdvisories(advisories, io) {
|
|
|
46
49
|
);
|
|
47
50
|
printReshapeDecisionsSection(advisories.physicalCohesion?.reshapeDecisions, io);
|
|
48
51
|
printParseHealthSection(advisories.parseHealth, io);
|
|
52
|
+
printGraphBlindSection(advisories.graphBlindSpots, io);
|
|
49
53
|
}
|
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -35,8 +35,12 @@ import {
|
|
|
35
35
|
buildPostGreenNextAction,
|
|
36
36
|
mergePostGreenTopActions,
|
|
37
37
|
isDoctorHealthyNothingToDo,
|
|
38
|
+
DESIGN_WEAK_HONESTY_FLAGS,
|
|
38
39
|
} from './post-green-path.mjs';
|
|
39
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
buildCoverageHonesty,
|
|
42
|
+
computeDoctorEnforcementHonesty,
|
|
43
|
+
} from './enforcement-honesty.mjs';
|
|
40
44
|
import {
|
|
41
45
|
computePureLayerOptInNudge,
|
|
42
46
|
loadGoldenPattern,
|
|
@@ -129,10 +133,12 @@ export function runCoverage(root, config, files, rules, asJson) {
|
|
|
129
133
|
if (files.length > 0 && governed.percent < 50) {
|
|
130
134
|
console.log('');
|
|
131
135
|
console.log(
|
|
132
|
-
`⚠ Ark governs a MINORITY of your code (${governed.percent}%). A green check
|
|
136
|
+
`⚠ Ark governs a MINORITY of your code (${governed.percent}%). A green check on ~${governed.percent}%`
|
|
137
|
+
);
|
|
138
|
+
console.log(
|
|
139
|
+
' is worse than no gate — it looks safe while most code is ungoverned. Classify the'
|
|
133
140
|
);
|
|
134
|
-
console.log('
|
|
135
|
-
console.log(' below to actually cover it.');
|
|
141
|
+
console.log(' directories below before treating green as enforcement.');
|
|
136
142
|
}
|
|
137
143
|
if (suggestions.length > 0) {
|
|
138
144
|
console.log('');
|
|
@@ -236,6 +242,11 @@ export function buildRemediationPlan(
|
|
|
236
242
|
patternBets,
|
|
237
243
|
designSmells,
|
|
238
244
|
});
|
|
245
|
+
const coverageHonesty = buildCoverageHonesty({
|
|
246
|
+
percent: governedPercent,
|
|
247
|
+
totalFiles,
|
|
248
|
+
emptyScope,
|
|
249
|
+
});
|
|
239
250
|
|
|
240
251
|
let statement =
|
|
241
252
|
activeViolations.length > 0
|
|
@@ -264,6 +275,7 @@ export function buildRemediationPlan(
|
|
|
264
275
|
? {
|
|
265
276
|
designWeakLabel:
|
|
266
277
|
'ENFORCE · design-weak — use patternBets / dual-plan B; never auto-apply as mechanical-safe',
|
|
278
|
+
...DESIGN_WEAK_HONESTY_FLAGS,
|
|
267
279
|
}
|
|
268
280
|
: {}),
|
|
269
281
|
...(governedPercent != null ? { governedPercent } : {}),
|
|
@@ -282,6 +294,7 @@ export function buildRemediationPlan(
|
|
|
282
294
|
designSmells,
|
|
283
295
|
// Q04: one-pilot loop step (extraction card); never mechanical-safe.
|
|
284
296
|
pilotLoop,
|
|
297
|
+
coverageHonesty,
|
|
285
298
|
};
|
|
286
299
|
}
|
|
287
300
|
|
|
@@ -435,6 +448,18 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
435
448
|
designSmells,
|
|
436
449
|
});
|
|
437
450
|
const doctorAdvisories = computeDoctorAdvisories(root, config, cov, rules, files, options.ts, options.parseHealth);
|
|
451
|
+
const { coverageHonesty, baselineHonesty, writePathHonesty } = computeDoctorEnforcementHonesty({
|
|
452
|
+
governedPercent: cov.governed.percent,
|
|
453
|
+
totalFiles: cov.governed.totalFiles,
|
|
454
|
+
emptyScope: cov.emptyScope === true || cov.governed.totalFiles === 0,
|
|
455
|
+
baselineExists: baseline.exists,
|
|
456
|
+
frozenKeys: baseline.exists ? baseline.keys.size : 0,
|
|
457
|
+
activeViolations: activeCount,
|
|
458
|
+
suppressed,
|
|
459
|
+
totalViolations: violations.length,
|
|
460
|
+
activeHost: writePath.activeHost,
|
|
461
|
+
hardWriteActive: writePath.capabilities?.['hard-write'] === true,
|
|
462
|
+
});
|
|
438
463
|
|
|
439
464
|
if (asJson) {
|
|
440
465
|
(options.writeJson ?? console.log)(
|
|
@@ -464,10 +489,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
464
489
|
// Q01: primary next action when Shape residual dominates (null if not design-weak).
|
|
465
490
|
postGreenPath,
|
|
466
491
|
...(postGreenPath
|
|
467
|
-
? {
|
|
468
|
-
primaryNextAction: postGreenPath.action,
|
|
469
|
-
healthyFinishedForbidden: true,
|
|
470
|
-
}
|
|
492
|
+
? { primaryNextAction: postGreenPath.action, ...DESIGN_WEAK_HONESTY_FLAGS }
|
|
471
493
|
: {}),
|
|
472
494
|
// Q03: advisory golden for new-code placement (absent = no claim).
|
|
473
495
|
goldenPattern,
|
|
@@ -475,10 +497,10 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
475
497
|
pureLayerOptIn,
|
|
476
498
|
// Q04: one-pilot loop (extraction card → re-doctor).
|
|
477
499
|
pilotLoop,
|
|
478
|
-
// Advisories, never a verdict: W01
|
|
479
|
-
// X04 physical cohesion/reshape pilot, Y03 parse health.
|
|
500
|
+
// Advisories, never a verdict: W01/U05/X04/Y03 + graph-blind spots.
|
|
480
501
|
...doctorAdvisories,
|
|
481
502
|
governed: cov.governed,
|
|
503
|
+
coverageHonesty,
|
|
482
504
|
emptyLayers: cov.emptyLayers,
|
|
483
505
|
layersWithoutRules: cov.layersWithoutRules,
|
|
484
506
|
ungovernedDirs: cov.suggestions.length,
|
|
@@ -497,6 +519,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
497
519
|
frozen: baseline.exists ? baseline.keys.size : 0,
|
|
498
520
|
stale: analysisComplete ? staleBaseline : null,
|
|
499
521
|
policy: adoption.baseline,
|
|
522
|
+
honesty: baselineHonesty,
|
|
500
523
|
},
|
|
501
524
|
gatesMissing,
|
|
502
525
|
skillGaps,
|
|
@@ -517,6 +540,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
517
540
|
hookRepair: writePath.hookRepair,
|
|
518
541
|
mcpPresent: writePath.mcpPresent,
|
|
519
542
|
evidence: writePath.evidence,
|
|
543
|
+
honesty: writePathHonesty,
|
|
520
544
|
...(writePath.sessionNote ? { sessionNote: writePath.sessionNote } : {}),
|
|
521
545
|
...(writePath.gap
|
|
522
546
|
? {
|
|
@@ -688,6 +712,12 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
688
712
|
? ok
|
|
689
713
|
: warn;
|
|
690
714
|
line(govMark, `Governed: ${cov.governed.percent}% (${cov.governed.classifiedFiles}/${cov.governed.totalFiles} files)`);
|
|
715
|
+
if (coverageHonesty.greenIsNotEnforcement) {
|
|
716
|
+
line(coverageHonesty.worseThanNoGate ? bad : warn, coverageHonesty.message);
|
|
717
|
+
if (coverageHonesty.worseThanNoGate) {
|
|
718
|
+
actions.push('raise governed coverage above a minority slice before treating green as enforcement');
|
|
719
|
+
}
|
|
720
|
+
}
|
|
691
721
|
if (cov.suggestions.length > 0) {
|
|
692
722
|
line(warn, `${cov.suggestions.length} ungoverned director(y/ies) — proposals: ${arkCommand(root, 'ark-check', '--coverage')}`);
|
|
693
723
|
actions.push('classify the ungoverned directories (/ark-contract)');
|
|
@@ -770,8 +800,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
770
800
|
line(' ', `Active host: ${writePath.activeHost}`);
|
|
771
801
|
line(' ', `Supported profile: ${writePath.supportSummary}`);
|
|
772
802
|
line(wpMark, `Mode: ${writePath.mode} — ${writePathLabels[writePath.mode] || writePath.mode}`);
|
|
773
|
-
|
|
774
|
-
if (honestyLine) line(warn, honestyLine);
|
|
803
|
+
if (writePathHonesty.message) line(warn, writePathHonesty.message);
|
|
775
804
|
if (writePath.sessionNote) {
|
|
776
805
|
line(warn, writePath.sessionNote);
|
|
777
806
|
}
|
|
@@ -850,7 +879,12 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
850
879
|
} else {
|
|
851
880
|
// Baseline keys are line-agnostic, so N keys can suppress ≥N violations — label as keys
|
|
852
881
|
// to avoid an apparent mismatch with the "frozen" violation count above.
|
|
853
|
-
|
|
882
|
+
const baseMark = !analysisComplete || baselineHonesty.dirtyBaselineRisk ? warn : ok;
|
|
883
|
+
line(baseMark, `${baseline.keys.size} frozen key(s)${analysisComplete ? '' : ' — stale comparison not verified'}`);
|
|
884
|
+
if (analysisComplete && baselineHonesty.dirtyBaselineRisk) {
|
|
885
|
+
line(warn, baselineHonesty.message);
|
|
886
|
+
actions.push('review dirty baseline freezes — fix the contract before trusting green-via-freeze');
|
|
887
|
+
}
|
|
854
888
|
if (analysisComplete && staleBaseline > 0) {
|
|
855
889
|
line(warn, `${staleBaseline} stale entr(y/ies) no longer occur — tighten with --update-baseline`);
|
|
856
890
|
actions.push('tighten the baseline (--update-baseline)');
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product honesty helpers — weak coverage, dirty baseline, soft write hosts.
|
|
3
|
+
*
|
|
4
|
+
* Pure / fail-closed: never invent hard write guarantees; never paint thin
|
|
5
|
+
* coverage or a dirty freeze as "done." Advisory labels only.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
doctorWritePathHonestyMessage,
|
|
9
|
+
HOST_SUPPORT_MATRIX,
|
|
10
|
+
HOST_SUPPORT_HOSTS,
|
|
11
|
+
} from './host-support-matrix.mjs';
|
|
12
|
+
|
|
13
|
+
/** Soft = matrix hard-write false; hard-capable = matrix hard-write true. Single source of truth. */
|
|
14
|
+
function hostWriteClassSets() {
|
|
15
|
+
const soft = new Set();
|
|
16
|
+
const hard = new Set();
|
|
17
|
+
for (const host of HOST_SUPPORT_HOSTS) {
|
|
18
|
+
const profile = HOST_SUPPORT_MATRIX[host];
|
|
19
|
+
if (profile?.capabilities?.['hard-write']) hard.add(host);
|
|
20
|
+
else soft.add(host);
|
|
21
|
+
}
|
|
22
|
+
return { soft, hard };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { soft: SOFT_WRITE_HOSTS, hard: HARD_WRITE_HOSTS } = hostWriteClassSets();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Coverage honesty: green on a minority of the tree is worse than no gate.
|
|
29
|
+
* `greenIsNotEnforcement` stays true until the whole in-scope tree is governed (100%).
|
|
30
|
+
* @param {{ percent?: number|null, totalFiles?: number|null, emptyScope?: boolean }} input
|
|
31
|
+
*/
|
|
32
|
+
export function buildCoverageHonesty(input = {}) {
|
|
33
|
+
const total = Number(input.totalFiles) || 0;
|
|
34
|
+
const empty = input.emptyScope === true || total === 0;
|
|
35
|
+
const percent = Number.isFinite(Number(input.percent)) ? Number(input.percent) : 0;
|
|
36
|
+
|
|
37
|
+
if (empty) {
|
|
38
|
+
return {
|
|
39
|
+
status: 'empty-scope',
|
|
40
|
+
advisory: true,
|
|
41
|
+
greenIsNotEnforcement: true,
|
|
42
|
+
worseThanNoGate: true,
|
|
43
|
+
wholeTreeGoverned: false,
|
|
44
|
+
governedPercent: 0,
|
|
45
|
+
// Always 0 when empty-scope so payload cannot contradict the message.
|
|
46
|
+
totalFiles: 0,
|
|
47
|
+
message:
|
|
48
|
+
'Empty scope: a green check matches 0 files and is worse than no gate until include/layers cover real code.',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (percent < 50) {
|
|
52
|
+
return {
|
|
53
|
+
status: 'weak',
|
|
54
|
+
advisory: true,
|
|
55
|
+
greenIsNotEnforcement: true,
|
|
56
|
+
worseThanNoGate: true,
|
|
57
|
+
wholeTreeGoverned: false,
|
|
58
|
+
governedPercent: percent,
|
|
59
|
+
totalFiles: total,
|
|
60
|
+
message: `Weak coverage (${percent}%): a green check on a minority of the tree is worse than no gate — most code is ungoverned.`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (percent < 80) {
|
|
64
|
+
return {
|
|
65
|
+
status: 'partial',
|
|
66
|
+
advisory: true,
|
|
67
|
+
greenIsNotEnforcement: true,
|
|
68
|
+
worseThanNoGate: false,
|
|
69
|
+
wholeTreeGoverned: false,
|
|
70
|
+
governedPercent: percent,
|
|
71
|
+
totalFiles: total,
|
|
72
|
+
message: `Partial coverage (${percent}%): green means edges on the governed slice only, not the whole tree.`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const wholeTree = percent >= 100;
|
|
76
|
+
return {
|
|
77
|
+
status: 'strong',
|
|
78
|
+
advisory: true,
|
|
79
|
+
// Strong slice ≠ full-tree enforcement; boolean consumers must not oversell.
|
|
80
|
+
greenIsNotEnforcement: !wholeTree,
|
|
81
|
+
worseThanNoGate: false,
|
|
82
|
+
wholeTreeGoverned: wholeTree,
|
|
83
|
+
governedPercent: percent,
|
|
84
|
+
totalFiles: total,
|
|
85
|
+
message: wholeTree
|
|
86
|
+
? `Governed 100% of in-scope files — green is meaningful for the full in-scope tree.`
|
|
87
|
+
: `Governed ${percent}% of in-scope files — green is meaningful only for that governed slice; ${100 - percent}% remains ungoverned.`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Baseline honesty: a large freeze that zeros active violations can look safe
|
|
93
|
+
* while hiding false-positive debt.
|
|
94
|
+
* @param {{
|
|
95
|
+
* exists?: boolean,
|
|
96
|
+
* frozenKeys?: number,
|
|
97
|
+
* activeViolations?: number,
|
|
98
|
+
* suppressed?: number,
|
|
99
|
+
* totalViolations?: number,
|
|
100
|
+
* }} input
|
|
101
|
+
*/
|
|
102
|
+
export function buildBaselineHonesty(input = {}) {
|
|
103
|
+
const exists = input.exists === true;
|
|
104
|
+
const frozen = Number(input.frozenKeys) || 0;
|
|
105
|
+
const active = Number(input.activeViolations) || 0;
|
|
106
|
+
const suppressed = Number(input.suppressed) || 0;
|
|
107
|
+
const total = Number(input.totalViolations) || 0;
|
|
108
|
+
|
|
109
|
+
if (!exists) {
|
|
110
|
+
return {
|
|
111
|
+
status: total > 0 ? 'missing-with-debt' : 'absent',
|
|
112
|
+
advisory: true,
|
|
113
|
+
dirtyBaselineRisk: false,
|
|
114
|
+
frozenKeys: 0,
|
|
115
|
+
activeViolations: active,
|
|
116
|
+
suppressed: 0,
|
|
117
|
+
message:
|
|
118
|
+
total > 0
|
|
119
|
+
? 'No baseline while violations exist — freeze only real debt after the contract is honest.'
|
|
120
|
+
: 'No baseline (nothing to freeze).',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const suppressShare = total > 0 ? suppressed / total : 0;
|
|
125
|
+
// Dirty: green-via-freeze with material frozen debt (false-positive risk).
|
|
126
|
+
const dirty =
|
|
127
|
+
active === 0 &&
|
|
128
|
+
frozen > 0 &&
|
|
129
|
+
(frozen >= 10 || (frozen >= 5 && suppressed > 0) || (total >= 3 && suppressShare >= 0.5));
|
|
130
|
+
|
|
131
|
+
if (dirty) {
|
|
132
|
+
return {
|
|
133
|
+
status: 'dirty-freeze',
|
|
134
|
+
advisory: true,
|
|
135
|
+
dirtyBaselineRisk: true,
|
|
136
|
+
frozenKeys: frozen,
|
|
137
|
+
activeViolations: active,
|
|
138
|
+
suppressed,
|
|
139
|
+
message: `Baseline freezes ${frozen} key(s) while active violations are ${active} — green may hide false-positive debt. Prefer contract fixes over a dirty freeze.`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (frozen > 0) {
|
|
143
|
+
return {
|
|
144
|
+
status: 'active-freeze',
|
|
145
|
+
advisory: true,
|
|
146
|
+
dirtyBaselineRisk: false,
|
|
147
|
+
frozenKeys: frozen,
|
|
148
|
+
activeViolations: active,
|
|
149
|
+
suppressed,
|
|
150
|
+
message: `${frozen} frozen key(s); new distinct violations still fail. Ratchet down; do not reopen.`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
status: 'empty-freeze',
|
|
155
|
+
advisory: true,
|
|
156
|
+
dirtyBaselineRisk: false,
|
|
157
|
+
frozenKeys: 0,
|
|
158
|
+
activeViolations: active,
|
|
159
|
+
suppressed,
|
|
160
|
+
message: 'Baseline present with 0 frozen keys — every violation is active (honest).',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Write-path honesty for the active host (fail-closed).
|
|
166
|
+
* Soft hosts never claim hard local write; hard hosts without proof stay unverified.
|
|
167
|
+
* @param {string|null|undefined} activeHost
|
|
168
|
+
* @param {boolean} hardWriteActive
|
|
169
|
+
*/
|
|
170
|
+
export function buildWritePathHonesty(activeHost, hardWriteActive = false) {
|
|
171
|
+
const host = typeof activeHost === 'string' ? activeHost.trim().toLowerCase() : '';
|
|
172
|
+
const softWriteHost = SOFT_WRITE_HOSTS.has(host);
|
|
173
|
+
const hardCapable = HARD_WRITE_HOSTS.has(host);
|
|
174
|
+
const message = doctorWritePathHonestyMessage(host, hardWriteActive);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
advisory: true,
|
|
178
|
+
activeHost: host || null,
|
|
179
|
+
softWriteHost,
|
|
180
|
+
hardWriteSupported: hardCapable,
|
|
181
|
+
hardWriteActive: Boolean(hardWriteActive) && hardCapable && !softWriteHost,
|
|
182
|
+
hardWriteUnverified: hardCapable && !hardWriteActive,
|
|
183
|
+
hardMergeBoundary: 'required-ci-status (arkgate-check --strict-merge)',
|
|
184
|
+
message,
|
|
185
|
+
// Explicit product rule for soft hosts.
|
|
186
|
+
...(softWriteHost
|
|
187
|
+
? {
|
|
188
|
+
note: 'Local write is advisory / best-effort — not a hard PreToolUse boundary. Required CI status is the hard merge boundary.',
|
|
189
|
+
}
|
|
190
|
+
: {}),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* One-shot doctor honesty bundle (coverage + baseline + write path).
|
|
196
|
+
* Keeps doctor-plan.mjs under its module budget.
|
|
197
|
+
*/
|
|
198
|
+
export function computeDoctorEnforcementHonesty({
|
|
199
|
+
governedPercent,
|
|
200
|
+
totalFiles,
|
|
201
|
+
emptyScope,
|
|
202
|
+
baselineExists,
|
|
203
|
+
frozenKeys,
|
|
204
|
+
activeViolations,
|
|
205
|
+
suppressed,
|
|
206
|
+
totalViolations,
|
|
207
|
+
activeHost,
|
|
208
|
+
hardWriteActive,
|
|
209
|
+
} = {}) {
|
|
210
|
+
return {
|
|
211
|
+
coverageHonesty: buildCoverageHonesty({
|
|
212
|
+
percent: governedPercent,
|
|
213
|
+
totalFiles,
|
|
214
|
+
emptyScope,
|
|
215
|
+
}),
|
|
216
|
+
baselineHonesty: buildBaselineHonesty({
|
|
217
|
+
exists: baselineExists,
|
|
218
|
+
frozenKeys,
|
|
219
|
+
activeViolations,
|
|
220
|
+
suppressed,
|
|
221
|
+
totalViolations,
|
|
222
|
+
}),
|
|
223
|
+
writePathHonesty: buildWritePathHonesty(activeHost, hardWriteActive),
|
|
224
|
+
};
|
|
225
|
+
}
|