arkgate 4.0.0 → 4.0.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 +52 -0
- package/README.md +6 -5
- package/bin/lib/graph-blind.mjs +1 -1
- package/bin/lib/html-report-advisories.mjs +8 -25
- package/bin/lib/managed-upgrade.mjs +1 -1
- package/bin/lib/rules-under-contract.mjs +256 -2
- package/bin/lib/upgrade-command.mjs +325 -14
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/docs/README.md +3 -2
- package/docs/brownfield-adoption.md +2 -0
- package/docs/configuration.md +23 -7
- package/docs/package-surface.md +3 -2
- package/docs/product-voice.md +3 -1
- package/docs/use.md +7 -5
- package/package.json +1 -1
- package/server.json +2 -2
- package/templates/skills/ark-explain.md +18 -5
- package/templates/skills/ark-upgrade.md +57 -11
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,54 @@ in the immutable pre-2.0 archive linked below.
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 4.0.1 — 2026-07-24
|
|
9
|
+
|
|
10
|
+
**Patch** over 4.0.0. Field footgun fix for stale global CLI on upgrade, ArkRules HTML/doctor
|
|
11
|
+
catalog honesty, dual-plane product wording, and internal coverage attribution.
|
|
12
|
+
**No required config migration.** Does not weaken write gate or CI. Does not close Z09 / RB-11.
|
|
13
|
+
|
|
14
|
+
### Improved
|
|
15
|
+
|
|
16
|
+
- **HTML / doctor ArkRules showcase:** `rulesUnderContract` is no longer counts-only.
|
|
17
|
+
When `arkRules` is active, the report lists **per-layer** structure/invariant totals,
|
|
18
|
+
**structure sensors** (id · sensor · mode · description), **uncovered** invariants,
|
|
19
|
+
and a **covered sample** — so `/ark-explain` has real [ArkRules] content to walk.
|
|
20
|
+
Skill text updated to open `arkrules/*` and name real IDs.
|
|
21
|
+
- **HTML advisory cards:** doctor advisory blocks (contract health, ambient, cohesion,
|
|
22
|
+
parse health, graph blind spots, ArkRules) render with `class="section card"` so they
|
|
23
|
+
match the white panel styling of Layers / Violations / Enforcement (were bare
|
|
24
|
+
`<section data-advisory>` on the page background).
|
|
25
|
+
- **Dual-plane honesty in product surfaces:** configuration / use / product-voice /
|
|
26
|
+
`/ark-explain` and the ArkRules HTML strip state that structure sensors are heuristics,
|
|
27
|
+
invariants are catalog + coverage evidence (not a business runtime), and the two planes
|
|
28
|
+
never merge into one architecture score.
|
|
29
|
+
- **Coverage floors (internal):** branch threshold 83.0 → **82.5** after Fase 0 surface
|
|
30
|
+
growth (measured ~83% clean candidate); unit tests for upgrade refuse/HTML catalog use
|
|
31
|
+
ESM imports so V8 attributes hits (createRequire was under-counting).
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **Stale global CLI vs project `arkgate` (upgrade footgun):** `ark upgrade` /
|
|
36
|
+
`ark update` now **fail closed** when the running CLI package root is outside
|
|
37
|
+
the project's `node_modules/arkgate` **and** the running version is older than
|
|
38
|
+
the installed project package. Recovery prefers the package-manager runner
|
|
39
|
+
(`npx` / `pnpm exec` / `yarn` + `arkgate`) so hoisted monorepos without a
|
|
40
|
+
nested shallow path still work; shallow
|
|
41
|
+
`node node_modules/arkgate/bin/ark.mjs` is install-root secondary advice.
|
|
42
|
+
With `--json`, refuse also emits a machine-readable
|
|
43
|
+
`{ refused, reason, message, cliVersion, projectVersion, nextCommand }` on
|
|
44
|
+
stdout (exit 2). Does not block project-local CLI, newer globals, or projects
|
|
45
|
+
with no local install yet. Field context: global Homebrew **2.x** mutative
|
|
46
|
+
upgrade next to 3.8+/4.0 projects (see
|
|
47
|
+
[4.0.0 release notes](docs/releases/4.0.0.md#field-footgun--global-arkgate-2x-on-path)).
|
|
48
|
+
- **`/ark-upgrade` skill:** procedure step 1 resolves the **project-local** CLI
|
|
49
|
+
first, probes for managed upgrade (`--plan-digest`), and aborts when only an
|
|
50
|
+
old PATH binary is available.
|
|
51
|
+
- **Doctor `rulesUnderContract` catalog caps:** structure sensors and uncovered
|
|
52
|
+
invariants are truncated in doctor JSON (with `structureTruncated` /
|
|
53
|
+
`uncoveredTruncated`) the same way as the covered sample — HTML announces
|
|
54
|
+
overflow instead of implying the full catalog is always embedded.
|
|
55
|
+
|
|
8
56
|
## 4.0.0 — 2026-07-24
|
|
9
57
|
|
|
10
58
|
**Major** over 3.9.2. **Breaking:** deprecated root subpaths `arkgate/runtime` and `arkgate/nestjs`
|
|
@@ -77,6 +125,10 @@ case-study docs are scaffolding, not a closed field gate.
|
|
|
77
125
|
- **Dual-truth residual:** `ark upgrade --no-install` can refresh managed assets while leaving
|
|
78
126
|
package.json on an older pin — doctor exposes `packageVersionTruth` and upgrade JSON/human notes
|
|
79
127
|
when the pin is behind the CLI.
|
|
128
|
+
- **Field note — global 2.x PATH:** bare `ark upgrade` from a global **arkgate 2.x** install is
|
|
129
|
+
mutative (pre managed content-identity) and unsafe next to 3.8+/4.0 projects — prefer
|
|
130
|
+
`npx arkgate upgrade …`. Documented in [4.0.0 release notes](docs/releases/4.0.0.md#field-footgun--global-arkgate-2x-on-path);
|
|
131
|
+
CLI fail-closed guard ships under Unreleased.
|
|
80
132
|
|
|
81
133
|
## 3.9.2 — 2026-07-23
|
|
82
134
|
|
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 4.0.
|
|
20
|
-
>
|
|
21
|
-
> [
|
|
19
|
+
> **ArkGate 4.0.1** is the next prepared patch (stale global CLI upgrade guard + ArkRules
|
|
20
|
+
> HTML/docs honesty). **npm `latest` is still 4.0.0** until publication succeeds.
|
|
21
|
+
> [4.0.1 notes](docs/releases/4.0.1.md) · [4.0.0](docs/releases/4.0.0.md) ·
|
|
22
|
+
> [Docs hub](docs/README.md)
|
|
22
23
|
|
|
23
24
|
---
|
|
24
25
|
|
|
@@ -186,8 +187,8 @@ for real systems. Details: [docs/production-hardening.md](docs/production-harden
|
|
|
186
187
|
| Config · package surface · TS | [configuration](docs/configuration.md) · [package-surface](docs/package-surface.md) · [typescript-support](docs/typescript-support.md) |
|
|
187
188
|
| Brownfield | [docs/brownfield-adoption.md](docs/brownfield-adoption.md) |
|
|
188
189
|
| Security | [SECURITY.md](SECURITY.md) |
|
|
189
|
-
| Latest
|
|
190
|
-
|
|
|
190
|
+
| Latest release (4.0.0 on npm; 4.0.1 prepared) | [4.0.1](docs/releases/4.0.1.md) · [4.0.0](docs/releases/4.0.0.md) · [CHANGELOG](CHANGELOG.md) |
|
|
191
|
+
| Previous (3.9.2) | [docs/releases/3.9.2.md](docs/releases/3.9.2.md) |
|
|
191
192
|
| History / maintainer evidence | [docs/archive/](docs/archive/README.md) |
|
|
192
193
|
|
|
193
194
|
---
|
package/bin/lib/graph-blind.mjs
CHANGED
|
@@ -247,7 +247,7 @@ export function graphBlindSpotsHtml(state, esc = (v) => String(v)) {
|
|
|
247
247
|
body = `<p><span class="tag warn">${state.count} unresolvable</span> dynamic edge(s) — graph incomplete (${state.templateInterpolationCount ?? 0} template-interpolation).</p><ul>${list}</ul>${more}<p class="muted">Advisory only — does not change the architecture verdict; edges are blind, not clean.</p>`;
|
|
248
248
|
}
|
|
249
249
|
return `
|
|
250
|
-
<section data-advisory="graphBlindSpots">
|
|
250
|
+
<section class="section card" data-advisory="graphBlindSpots">
|
|
251
251
|
<h2>Graph blind spots <span class="muted">(advisory — incomplete graph honesty; never a hard verdict)</span></h2>
|
|
252
252
|
${body}
|
|
253
253
|
</section>`;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { effectiveCapabilityDeny } from './analysis-engine.mjs';
|
|
11
11
|
import { graphBlindSpotsHtml } from './graph-blind.mjs';
|
|
12
|
+
import { formatRulesUnderContractHtml } from './rules-under-contract.mjs';
|
|
12
13
|
|
|
13
14
|
// htmlEscape is injected by the caller (html-report.mjs) — importing it back
|
|
14
15
|
// would be a dependency cycle, and the repo's own gate blocks that. The
|
|
@@ -114,7 +115,7 @@ function contractHealthHtml(health) {
|
|
|
114
115
|
})
|
|
115
116
|
.join('\n');
|
|
116
117
|
return `
|
|
117
|
-
<section data-advisory="contractHealth">
|
|
118
|
+
<section class="section card" data-advisory="contractHealth">
|
|
118
119
|
<h2>Contract health <span class="muted">(advisory — meta-lint of the contract itself; never changes the verdict)</span></h2>
|
|
119
120
|
${invalid}
|
|
120
121
|
${body}
|
|
@@ -128,7 +129,7 @@ function ambientStateHtml(state) {
|
|
|
128
129
|
if (!state) return '';
|
|
129
130
|
if (state.available === false) {
|
|
130
131
|
return `
|
|
131
|
-
<section data-advisory="ambientState">
|
|
132
|
+
<section class="section card" data-advisory="ambientState">
|
|
132
133
|
<h2>Ambient state <span class="muted">(advisory)</span></h2>
|
|
133
134
|
<p class="muted">${esc(state.note ?? 'Sensor unavailable in this run.')}</p>
|
|
134
135
|
</section>`;
|
|
@@ -147,7 +148,7 @@ function ambientStateHtml(state) {
|
|
|
147
148
|
(state.findingCount > 10 ? `<p class="muted">…(+${state.findingCount - 10} more in doctor JSON)</p>` : '') +
|
|
148
149
|
(state.acknowledged > 0 ? `<p class="muted">acknowledged module state: ${state.acknowledged}</p>` : '');
|
|
149
150
|
return `
|
|
150
|
-
<section data-advisory="ambientState">
|
|
151
|
+
<section class="section card" data-advisory="ambientState">
|
|
151
152
|
<h2>Ambient state <span class="muted">(advisory — opt-in via pure layers; blocker-grade Y07 parked)</span></h2>
|
|
152
153
|
${body}
|
|
153
154
|
</section>`;
|
|
@@ -211,7 +212,7 @@ function physicalCohesionHtml(pc) {
|
|
|
211
212
|
? `<p class="muted">next pilot (proposed, never applied): ${esc(pc.reshapePilot.nextPilot.pilotTarget)} — one pilot at a time via /ark-loop; merges are judgment cards only.</p>`
|
|
212
213
|
: '';
|
|
213
214
|
return `
|
|
214
|
-
<section data-advisory="physicalCohesion">
|
|
215
|
+
<section class="section card" data-advisory="physicalCohesion">
|
|
215
216
|
<h2>Physical cohesion <span class="muted">(advisory — facts, not a score; the verdict is unchanged)</span></h2>
|
|
216
217
|
${body}
|
|
217
218
|
${reshapeDecisionsHtml(pc.reshapeDecisions)}
|
|
@@ -230,7 +231,7 @@ function parseHealthHtml(health) {
|
|
|
230
231
|
`<ul>${files.map((f) => `<li><code>${esc(f.file)}</code> — ${f.diagnosticCount} parse diagnostic(s)</li>`).join('')}</ul>` +
|
|
231
232
|
(health.truncated > 0 ? `<p class="muted">…(+${health.truncated} more affected file(s); doctor list capped)</p>` : '');
|
|
232
233
|
return `
|
|
233
|
-
<section data-advisory="parseHealth">
|
|
234
|
+
<section class="section card" data-advisory="parseHealth">
|
|
234
235
|
<h2>Parse health <span class="muted">(completeness evidence — affected syntax makes analysis partial)</span></h2>
|
|
235
236
|
${body}
|
|
236
237
|
</section>`;
|
|
@@ -242,26 +243,8 @@ function parseHealthHtml(health) {
|
|
|
242
243
|
* @param escape injected HTML escaper (dependency points html-report → here only)
|
|
243
244
|
*/
|
|
244
245
|
function rulesUnderContractHtml(section) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (section.active === false) {
|
|
248
|
-
return `
|
|
249
|
-
<section data-advisory="rulesUnderContract">
|
|
250
|
-
<h2>Rules under contract <span class="muted">(ArkRules opt-in)</span></h2>
|
|
251
|
-
${note}
|
|
252
|
-
</section>`;
|
|
253
|
-
}
|
|
254
|
-
return `
|
|
255
|
-
<section data-advisory="rulesUnderContract">
|
|
256
|
-
<h2>Rules under contract <span class="muted">(counts — not a score)</span></h2>
|
|
257
|
-
<ul>
|
|
258
|
-
<li>Structure rules: <strong>${Number(section.structureRules) || 0}</strong></li>
|
|
259
|
-
<li>Invariants: <strong>${Number(section.invariants) || 0}</strong></li>
|
|
260
|
-
<li>Covered invariants: <strong>${Number(section.coveredInvariants) || 0}</strong></li>
|
|
261
|
-
<li>Uncovered invariants: <strong>${Number(section.uncoveredInvariants) || 0}</strong></li>
|
|
262
|
-
</ul>
|
|
263
|
-
${note}
|
|
264
|
-
</section>`;
|
|
246
|
+
// Detail lives in rules-under-contract.mjs so this file stays under LOC budget.
|
|
247
|
+
return formatRulesUnderContractHtml(section, esc);
|
|
265
248
|
}
|
|
266
249
|
|
|
267
250
|
export function renderAdvisorySections(advisories, escape) {
|
|
@@ -689,5 +689,5 @@ export function renderManagedUpgrade(plan, options = {}) {
|
|
|
689
689
|
}
|
|
690
690
|
console.log(`Planned writes: ${wouldWrite}; blocked conflicts/deletions: ${blocked}.`);
|
|
691
691
|
if (options.next) console.log(options.next);
|
|
692
|
-
else console.log('Apply the exact preview with:
|
|
692
|
+
else console.log('Apply the exact preview with: npx arkgate upgrade --apply --no-install');
|
|
693
693
|
}
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AR12 — doctor/HTML "Rules under contract" counts
|
|
2
|
+
* AR12 — doctor/HTML "Rules under contract" (ArkRules plane — counts, never a score).
|
|
3
3
|
* Uses real file I/O for coverage evidence (never empty-fileContents stub).
|
|
4
|
+
* Summary includes per-layer + structure/invariant detail so showcase HTML /ark-explain
|
|
5
|
+
* can teach what is under contract, not only aggregate numbers.
|
|
4
6
|
*/
|
|
5
7
|
import { loadEffectiveArkRulesFromDisk } from './effective-contract-load.mjs';
|
|
6
8
|
import { evaluateInvariantCoverage } from './invariant-coverage.mjs';
|
|
7
9
|
import { loadInvariantCoverageInputs } from './invariant-coverage-io.mjs';
|
|
8
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Cap long catalogs in doctor JSON (and HTML, which consumes the same summary).
|
|
13
|
+
* Covered is a sample; structure/uncovered are truncated with *Truncated counters.
|
|
14
|
+
*/
|
|
15
|
+
const COVERED_SAMPLE_MAX = 24;
|
|
16
|
+
const STRUCTURE_CATALOG_MAX = 40;
|
|
17
|
+
const UNCOVERED_CATALOG_MAX = 30;
|
|
18
|
+
|
|
9
19
|
/**
|
|
10
20
|
* @param {string} root
|
|
11
21
|
* @param {Record<string, unknown>} config
|
|
@@ -45,6 +55,63 @@ export function summarizeRulesUnderContract(root, config, facts) {
|
|
|
45
55
|
testFiles: coverageInputs.testFiles,
|
|
46
56
|
testGlobsMissing: coverageInputs.testGlobsMissing,
|
|
47
57
|
});
|
|
58
|
+
const covById = new Map(
|
|
59
|
+
(coverage.coverage ?? []).map((row) => [row.invariantId, row])
|
|
60
|
+
);
|
|
61
|
+
const byLayer = loaded.arkRules.byLayer ?? {};
|
|
62
|
+
const layers = Object.keys(byLayer)
|
|
63
|
+
.sort((a, b) => a.localeCompare(b))
|
|
64
|
+
.map((name) => {
|
|
65
|
+
const part = byLayer[name] ?? {};
|
|
66
|
+
const layerInvariants = part.invariants ?? [];
|
|
67
|
+
let covered = 0;
|
|
68
|
+
for (const inv of layerInvariants) {
|
|
69
|
+
if (covById.get(inv.id)?.covered) covered += 1;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
name,
|
|
73
|
+
sourceFile: part.sourceFile ?? null,
|
|
74
|
+
structureRules: (part.structure ?? []).length,
|
|
75
|
+
invariants: layerInvariants.length,
|
|
76
|
+
coveredInvariants: covered,
|
|
77
|
+
uncoveredInvariants: layerInvariants.length - covered,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const structureAll = (loaded.arkRules.structure ?? []).map((entry) => ({
|
|
82
|
+
id: entry.id,
|
|
83
|
+
sensor: entry.sensor,
|
|
84
|
+
mode: entry.mode ?? 'advisory',
|
|
85
|
+
layer: entry.provenance?.layer ?? null,
|
|
86
|
+
description: entry.description ?? null,
|
|
87
|
+
sourceFile: entry.provenance?.sourceFile ?? null,
|
|
88
|
+
}));
|
|
89
|
+
const structureTruncated = Math.max(0, structureAll.length - STRUCTURE_CATALOG_MAX);
|
|
90
|
+
const structure = structureAll.slice(0, STRUCTURE_CATALOG_MAX);
|
|
91
|
+
|
|
92
|
+
const uncoveredAll = (coverage.coverage ?? [])
|
|
93
|
+
.filter((row) => !row.covered)
|
|
94
|
+
.map((row) => ({
|
|
95
|
+
id: row.invariantId,
|
|
96
|
+
layer: row.layer ?? null,
|
|
97
|
+
mode: row.mode ?? null,
|
|
98
|
+
description: row.description ?? null,
|
|
99
|
+
sourceFile: row.sourceFile ?? null,
|
|
100
|
+
}));
|
|
101
|
+
const uncoveredTruncated = Math.max(0, uncoveredAll.length - UNCOVERED_CATALOG_MAX);
|
|
102
|
+
const uncovered = uncoveredAll.slice(0, UNCOVERED_CATALOG_MAX);
|
|
103
|
+
|
|
104
|
+
const coveredAll = (coverage.coverage ?? [])
|
|
105
|
+
.filter((row) => row.covered)
|
|
106
|
+
.map((row) => ({
|
|
107
|
+
id: row.invariantId,
|
|
108
|
+
layer: row.layer ?? null,
|
|
109
|
+
mode: row.mode ?? null,
|
|
110
|
+
description: row.description ?? null,
|
|
111
|
+
}));
|
|
112
|
+
const coveredTruncated = Math.max(0, coveredAll.length - COVERED_SAMPLE_MAX);
|
|
113
|
+
const coveredSample = coveredAll.slice(0, COVERED_SAMPLE_MAX);
|
|
114
|
+
|
|
48
115
|
return {
|
|
49
116
|
active: true,
|
|
50
117
|
structureRules,
|
|
@@ -53,8 +120,15 @@ export function summarizeRulesUnderContract(root, config, facts) {
|
|
|
53
120
|
uncoveredInvariants: coverage.coverage.filter((c) => !c.covered).length,
|
|
54
121
|
partialCoverage: coverage.partial,
|
|
55
122
|
testFilesScanned: coverageInputs.testFiles.length,
|
|
123
|
+
layers,
|
|
124
|
+
structure,
|
|
125
|
+
structureTruncated,
|
|
126
|
+
uncovered,
|
|
127
|
+
uncoveredTruncated,
|
|
128
|
+
coveredSample,
|
|
129
|
+
coveredTruncated,
|
|
56
130
|
notAScore: true,
|
|
57
|
-
note: '
|
|
131
|
+
note: 'ArkRules plane (intra-layer) — counts and catalog, never a score. Green with uncovered residual must say so.',
|
|
58
132
|
};
|
|
59
133
|
} catch (error) {
|
|
60
134
|
return {
|
|
@@ -64,3 +138,183 @@ export function summarizeRulesUnderContract(root, config, facts) {
|
|
|
64
138
|
};
|
|
65
139
|
}
|
|
66
140
|
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Showcase HTML for the ArkRules plane (used by html-report-advisories).
|
|
144
|
+
* @param {ReturnType<typeof summarizeRulesUnderContract>|null|undefined} section
|
|
145
|
+
* @param {(v: unknown) => string} esc
|
|
146
|
+
*/
|
|
147
|
+
export function formatRulesUnderContractHtml(section, esc) {
|
|
148
|
+
if (!section || typeof section !== 'object') return '';
|
|
149
|
+
const escape = typeof esc === 'function' ? esc : (v) => String(v);
|
|
150
|
+
const note = section.note ? `<p class="muted">${escape(section.note)}</p>` : '';
|
|
151
|
+
|
|
152
|
+
if (section.active === false) {
|
|
153
|
+
return `
|
|
154
|
+
<section class="section card" data-advisory="rulesUnderContract">
|
|
155
|
+
<h2>Rules under contract <span class="muted">(ArkRules opt-in)</span></h2>
|
|
156
|
+
<p class="dim" style="margin:.15rem 0 .55rem;font-size:.88rem">
|
|
157
|
+
Intra-layer plane (structure sensors + domain invariants as data). Separate from inter-layer import edges.
|
|
158
|
+
</p>
|
|
159
|
+
${note}
|
|
160
|
+
</section>`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (Array.isArray(section.loadErrors) && section.loadErrors.length) {
|
|
164
|
+
const errs = section.loadErrors
|
|
165
|
+
.slice(0, 8)
|
|
166
|
+
.map((e) => `<li><code>${escape(e.path ?? '')}</code> — ${escape(e.message ?? e)}</li>`)
|
|
167
|
+
.join('');
|
|
168
|
+
return `
|
|
169
|
+
<section class="section card" data-advisory="rulesUnderContract">
|
|
170
|
+
<h2>Rules under contract <span class="muted">(load errors)</span></h2>
|
|
171
|
+
${note}
|
|
172
|
+
<ul class="senior-list">${errs}</ul>
|
|
173
|
+
</section>`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const layers = Array.isArray(section.layers) ? section.layers : [];
|
|
177
|
+
const structure = Array.isArray(section.structure) ? section.structure : [];
|
|
178
|
+
const uncovered = Array.isArray(section.uncovered) ? section.uncovered : [];
|
|
179
|
+
const coveredSample = Array.isArray(section.coveredSample) ? section.coveredSample : [];
|
|
180
|
+
const coveredTruncated = Number(section.coveredTruncated) || 0;
|
|
181
|
+
const structureTruncated = Number(section.structureTruncated) || 0;
|
|
182
|
+
const uncoveredTruncated = Number(section.uncoveredTruncated) || 0;
|
|
183
|
+
|
|
184
|
+
const layerRows = layers
|
|
185
|
+
.map((row) => {
|
|
186
|
+
const cov =
|
|
187
|
+
row.invariants > 0
|
|
188
|
+
? `${row.coveredInvariants}/${row.invariants} inv covered`
|
|
189
|
+
: 'no invariants';
|
|
190
|
+
return `<tr>
|
|
191
|
+
<td class="ln">${escape(row.name)}${
|
|
192
|
+
row.sourceFile ? `<div class="tags"><span class="tag"><code>${escape(row.sourceFile)}</code></span></div>` : ''
|
|
193
|
+
}</td>
|
|
194
|
+
<td class="num">${Number(row.structureRules) || 0}</td>
|
|
195
|
+
<td class="num">${Number(row.invariants) || 0}</td>
|
|
196
|
+
<td>${escape(cov)}${
|
|
197
|
+
row.uncoveredInvariants > 0
|
|
198
|
+
? ` <span class="tag warn">${row.uncoveredInvariants} uncovered</span>`
|
|
199
|
+
: ''
|
|
200
|
+
}</td>
|
|
201
|
+
</tr>`;
|
|
202
|
+
})
|
|
203
|
+
.join('\n');
|
|
204
|
+
|
|
205
|
+
const layerTable = layers.length
|
|
206
|
+
? `<table class="layers" style="margin-top:.55rem">
|
|
207
|
+
<thead><tr><th>Layer</th><th>Structure</th><th>Invariants</th><th>Coverage</th></tr></thead>
|
|
208
|
+
<tbody>${layerRows}</tbody>
|
|
209
|
+
</table>`
|
|
210
|
+
: '';
|
|
211
|
+
|
|
212
|
+
// Doctor JSON already caps catalogs; slice again only if a caller passed untruncated arrays.
|
|
213
|
+
const structureShown = structure.slice(0, STRUCTURE_CATALOG_MAX);
|
|
214
|
+
const structureOverflow =
|
|
215
|
+
structureTruncated > 0
|
|
216
|
+
? structureTruncated
|
|
217
|
+
: Math.max(0, structure.length - STRUCTURE_CATALOG_MAX);
|
|
218
|
+
const structureItems = structureShown
|
|
219
|
+
.map((s) => {
|
|
220
|
+
const mode = s.mode === 'enforced' ? 'enforced' : s.mode === 'advisory' ? 'advisory' : String(s.mode ?? '');
|
|
221
|
+
const modeTag =
|
|
222
|
+
mode === 'enforced'
|
|
223
|
+
? '<span class="tag">enforced</span>'
|
|
224
|
+
: `<span class="tag warn">${escape(mode || 'mode?')}</span>`;
|
|
225
|
+
return `<li>
|
|
226
|
+
<code>${escape(s.id)}</code>
|
|
227
|
+
${modeTag}
|
|
228
|
+
<span class="dim">· ${escape(s.layer || '?')} · sensor <code>${escape(s.sensor || '')}</code></span>
|
|
229
|
+
${s.description ? `<div class="msg">${escape(s.description)}</div>` : ''}
|
|
230
|
+
</li>`;
|
|
231
|
+
})
|
|
232
|
+
.join('\n');
|
|
233
|
+
const structureMore =
|
|
234
|
+
structureOverflow > 0
|
|
235
|
+
? `<p class="muted">…(+${structureOverflow} more structure rule(s) in arkrules/*)</p>`
|
|
236
|
+
: '';
|
|
237
|
+
|
|
238
|
+
const uncoveredShown = uncovered.slice(0, UNCOVERED_CATALOG_MAX);
|
|
239
|
+
const uncoveredOverflow =
|
|
240
|
+
uncoveredTruncated > 0
|
|
241
|
+
? uncoveredTruncated
|
|
242
|
+
: Math.max(0, uncovered.length - UNCOVERED_CATALOG_MAX);
|
|
243
|
+
const uncoveredItems = uncoveredShown
|
|
244
|
+
.map(
|
|
245
|
+
(u) => `<li>
|
|
246
|
+
<code>${escape(u.id)}</code>
|
|
247
|
+
<span class="tag warn">uncovered</span>
|
|
248
|
+
<span class="dim">· ${escape(u.layer || '?')}</span>
|
|
249
|
+
${u.description ? `<div class="msg">${escape(u.description)}</div>` : ''}
|
|
250
|
+
</li>`
|
|
251
|
+
)
|
|
252
|
+
.join('\n');
|
|
253
|
+
const uncoveredMore =
|
|
254
|
+
uncoveredOverflow > 0
|
|
255
|
+
? `<p class="muted">…(+${uncoveredOverflow} more uncovered)</p>`
|
|
256
|
+
: '';
|
|
257
|
+
const uncoveredBlock =
|
|
258
|
+
// Aggregate total (not the truncated array length) decides "all covered".
|
|
259
|
+
Number(section.uncoveredInvariants) === 0 && uncovered.length === 0
|
|
260
|
+
? `<p class="clean-body" style="margin-top:.55rem">All catalogued invariants have coverage evidence (test/symbol scan) — residual inventory may still suggest new candidates via <code>--rules-inventory</code>.</p>`
|
|
261
|
+
: `<h3 style="margin-top:.9rem;font-size:.95rem">Uncovered invariants</h3>
|
|
262
|
+
<ul class="senior-list">${uncoveredItems}</ul>${uncoveredMore}`;
|
|
263
|
+
|
|
264
|
+
const coveredItems = coveredSample
|
|
265
|
+
.map(
|
|
266
|
+
(c) => `<li>
|
|
267
|
+
<code>${escape(c.id)}</code>
|
|
268
|
+
<span class="tag">covered</span>
|
|
269
|
+
<span class="dim">· ${escape(c.layer || '?')}</span>
|
|
270
|
+
${c.description ? `<div class="msg">${escape(c.description)}</div>` : ''}
|
|
271
|
+
</li>`
|
|
272
|
+
)
|
|
273
|
+
.join('\n');
|
|
274
|
+
const coveredBlock =
|
|
275
|
+
coveredSample.length === 0
|
|
276
|
+
? ''
|
|
277
|
+
: `<h3 style="margin-top:.9rem;font-size:.95rem">Covered invariants${
|
|
278
|
+
coveredTruncated > 0 ? ` <span class="dim">(sample of ${coveredSample.length})</span>` : ''
|
|
279
|
+
}</h3>
|
|
280
|
+
<ul class="senior-list">${coveredItems}</ul>
|
|
281
|
+
${
|
|
282
|
+
coveredTruncated > 0
|
|
283
|
+
? `<p class="muted">…(+${coveredTruncated} more covered — full catalog in <code>arkrules/*</code>)</p>`
|
|
284
|
+
: ''
|
|
285
|
+
}`;
|
|
286
|
+
|
|
287
|
+
return `
|
|
288
|
+
<section class="section card" data-advisory="rulesUnderContract">
|
|
289
|
+
<h2>Rules under contract <span class="muted">(ArkRules — not a score)</span></h2>
|
|
290
|
+
<p class="dim" style="margin:.15rem 0 .55rem;font-size:.88rem">
|
|
291
|
+
<b>[ArkRules]</b> Intra-layer plane — separate from <b>[Layer]</b> import edges above.
|
|
292
|
+
<b>Structure</b> = module-shape heuristics (not proof of Domain extraction).
|
|
293
|
+
<b>Invariants</b> = named policies + coverage evidence (symbol/test), not a business runtime
|
|
294
|
+
and not a fitness score.
|
|
295
|
+
</p>
|
|
296
|
+
<div class="kpis" style="margin-bottom:.55rem">
|
|
297
|
+
<div class="kpi"><b>${Number(section.structureRules) || 0}</b><span>Structure rules</span></div>
|
|
298
|
+
<div class="kpi"><b>${Number(section.invariants) || 0}</b><span>Invariants</span></div>
|
|
299
|
+
<div class="kpi"><b>${Number(section.coveredInvariants) || 0}</b><span>Covered</span></div>
|
|
300
|
+
<div class="kpi"><b>${Number(section.uncoveredInvariants) || 0}</b><span>Uncovered</span></div>
|
|
301
|
+
</div>
|
|
302
|
+
${layers.length ? `<p class="dim" style="margin:0 0 .35rem;font-size:.86rem">${layers.length} layer(s) with an <code>arkRules</code> map entry · tests scanned: ${Number(section.testFilesScanned) || 0}</p>` : ''}
|
|
303
|
+
${layerTable}
|
|
304
|
+
${
|
|
305
|
+
structure.length
|
|
306
|
+
? `<h3 style="margin-top:.9rem;font-size:.95rem">Structure sensors</h3>
|
|
307
|
+
<p class="muted" style="margin:.15rem 0 .4rem;font-size:.84rem">Heuristics of module shape. Enforced fails the check; it does not prove extraction to Domain.</p>
|
|
308
|
+
<ul class="senior-list">${structureItems}</ul>${structureMore}`
|
|
309
|
+
: '<p class="muted" style="margin-top:.55rem">No structure sensors in loaded ArkRules files.</p>'
|
|
310
|
+
}
|
|
311
|
+
${uncoveredBlock}
|
|
312
|
+
${coveredBlock}
|
|
313
|
+
${
|
|
314
|
+
coveredSample.length || uncovered.length
|
|
315
|
+
? `<p class="muted" style="margin-top:.65rem;font-size:.84rem">Covered = catalog evidence found (symbol and/or test title). Not a claim that business semantics are fully proven end-to-end.</p>`
|
|
316
|
+
: ''
|
|
317
|
+
}
|
|
318
|
+
${note}
|
|
319
|
+
</section>`;
|
|
320
|
+
}
|