arkgate 4.6.6 → 4.7.0
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 +139 -2
- package/README.md +22 -10
- package/SECURITY.md +1 -1
- package/bin/ark-check-runtime.mjs +21 -341
- package/bin/ark-mcp-runtime.mjs +71 -325
- package/bin/ark-shared.mjs +24 -158
- package/bin/lib/adapter-contract.mjs +17 -36
- package/bin/lib/analysis-engine.mjs +6 -6
- package/bin/lib/ark-run-doctor.mjs +144 -0
- package/bin/lib/ark-run-facts.mjs +472 -0
- package/bin/lib/ark-run-report.mjs +57 -0
- package/bin/lib/ark-run-sensors.mjs +309 -0
- package/bin/lib/check-args.mjs +173 -0
- package/bin/lib/check-config-detect.mjs +101 -0
- package/bin/lib/check-watch.mjs +80 -0
- package/bin/lib/config-contract.mjs +86 -11
- package/bin/lib/deep-module-coach.mjs +3 -0
- package/bin/lib/diagnostic-catalog.mjs +8 -0
- package/bin/lib/doctor-advisories.mjs +45 -8
- package/bin/lib/doctor-human.mjs +519 -0
- package/bin/lib/doctor-plan.mjs +62 -445
- package/bin/lib/extra-merge-teeth.mjs +187 -0
- package/bin/lib/github-enforcement.mjs +22 -9
- package/bin/lib/html-report-advisories.mjs +2 -0
- package/bin/lib/html-report-depth.mjs +22 -2
- package/bin/lib/html-report.mjs +40 -7
- package/bin/lib/mcp-hook-payload.mjs +328 -0
- package/bin/lib/package-manager.mjs +174 -0
- package/bin/lib/policy-delta-io.mjs +4 -0
- package/bin/lib/remediation.mjs +132 -0
- package/bin/lib/resolved-candidate-facts.mjs +67 -2
- package/bin/lib/rules-under-contract.mjs +37 -89
- package/bin/lib/snippet-analysis.mjs +43 -2
- package/bin/lib/status-command.mjs +28 -0
- package/bin/lib/status-manifest.mjs +23 -0
- package/bin/lib/team-parliament-io.mjs +4 -0
- package/dist/{configTypes-l6XiwiC1.d.ts → configTypes-CgJimx9o.d.ts} +17 -3
- package/dist/eslint/index.cjs +6 -2
- package/dist/eslint/index.d.ts +70 -2
- package/dist/eslint/index.js +6 -2
- package/dist/index.cjs +35 -35
- package/dist/index.d.ts +787 -272
- package/dist/index.js +35 -35
- package/docs/README.md +4 -3
- package/docs/agent-guide.md +21 -15
- package/docs/ai-gates.md +13 -0
- package/docs/configuration.md +24 -11
- package/docs/develop.md +12 -3
- package/docs/diagnostics.md +75 -0
- package/docs/enthusiast/README.md +4 -3
- package/docs/package-surface.md +17 -13
- package/docs/product-voice.md +6 -3
- package/docs/threat-model.md +1 -1
- package/docs/use.md +5 -4
- package/package.json +1 -1
- package/schemas/ark.config.schema.json +41 -2
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
- package/schemas/ark.status-manifest.schema.json +47 -0
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +23 -2
- package/templates/agent-skills/ark-place/SKILL.md +26 -2
- package/templates/agent-skills/ark-runtime/SKILL.md +66 -24
- package/templates/skills/ark-adopt.md +23 -2
- package/templates/skills/ark-place.md +26 -2
- package/templates/skills/ark-runtime.md +66 -24
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE — do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* Canonical algorithm: src/domain/extraMergeTeeth.ts
|
|
5
|
+
* Regenerate: node scripts/generate-cli-pure.mjs
|
|
6
|
+
* Drift check: node scripts/generate-cli-pure.mjs --check
|
|
7
|
+
*
|
|
8
|
+
* Pure CLI helper (bin/lib/extra-merge-teeth.mjs). Zero Node I/O.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const EXTRA_MERGE_TEETH_GOVERNED_FLOOR = 50;
|
|
12
|
+
export function normalizeExtraMergeTeethClassification(classification) {
|
|
13
|
+
if (!classification)
|
|
14
|
+
return {};
|
|
15
|
+
const governedPercent = typeof classification.governedPercent === 'number' ? classification.governedPercent : null;
|
|
16
|
+
let populatedLayerCount = typeof classification.populatedLayerCount === 'number'
|
|
17
|
+
? classification.populatedLayerCount
|
|
18
|
+
: null;
|
|
19
|
+
if (populatedLayerCount == null &&
|
|
20
|
+
typeof classification.classifiedFiles === 'number') {
|
|
21
|
+
populatedLayerCount = classification.classifiedFiles > 0 ? 1 : 0;
|
|
22
|
+
}
|
|
23
|
+
return { governedPercent, populatedLayerCount };
|
|
24
|
+
}
|
|
25
|
+
export function extraMergeTeethAllowed(classification) {
|
|
26
|
+
const normalized = normalizeExtraMergeTeethClassification(classification);
|
|
27
|
+
const governed = typeof normalized.governedPercent === 'number' ? normalized.governedPercent : null;
|
|
28
|
+
const populated = typeof normalized.populatedLayerCount === 'number' ? normalized.populatedLayerCount : null;
|
|
29
|
+
if (governed == null && populated == null)
|
|
30
|
+
return true;
|
|
31
|
+
return ((governed ?? 0) >= EXTRA_MERGE_TEETH_GOVERNED_FLOOR && (populated ?? 0) >= 1);
|
|
32
|
+
}
|
|
33
|
+
export function classifyResolvedLayerCoverage(files) {
|
|
34
|
+
const total = files.length;
|
|
35
|
+
let classified = 0;
|
|
36
|
+
const populated = new Set();
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
const layer = typeof file.layer === 'string' && file.layer.length > 0 ? file.layer : null;
|
|
39
|
+
if (!layer)
|
|
40
|
+
continue;
|
|
41
|
+
classified += 1;
|
|
42
|
+
populated.add(layer);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
governedPercent: total > 0 ? Math.round((classified / total) * 100) : 0,
|
|
46
|
+
populatedLayerCount: populated.size,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function isArkRunRuleId(ruleId) {
|
|
50
|
+
return typeof ruleId === 'string' && ruleId.startsWith('ARKRUN_');
|
|
51
|
+
}
|
|
52
|
+
export function isExtraPlaneFinding(violation) {
|
|
53
|
+
if (violation?.arkruleId != null)
|
|
54
|
+
return true;
|
|
55
|
+
const id = typeof violation?.ruleId === 'string' ? violation.ruleId : '';
|
|
56
|
+
return id.startsWith('ARKRULE') || id.startsWith('arkrule') || id.startsWith('ARKRUN_');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Under the classification floor, demote enforced extra-plane findings in place
|
|
60
|
+
* so merge/write/CI match (layer graph only). Unknown classification is a no-op.
|
|
61
|
+
*/
|
|
62
|
+
export function demoteExtraPlaneTeethUnderClassificationFloor(violations, classification = {}) {
|
|
63
|
+
if (!Array.isArray(violations))
|
|
64
|
+
return violations;
|
|
65
|
+
if (extraMergeTeethAllowed(classification))
|
|
66
|
+
return violations;
|
|
67
|
+
for (const violation of violations) {
|
|
68
|
+
if (isExtraPlaneFinding(violation) && violation.failsStrict !== false) {
|
|
69
|
+
violation.failsStrict = false;
|
|
70
|
+
if (violation.severity === 'error')
|
|
71
|
+
violation.severity = 'warning';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return violations;
|
|
75
|
+
}
|
|
76
|
+
/** Stamp for extra-plane honesty: never one architecture score. */
|
|
77
|
+
export const MERGE_PLANES_DUAL_STAMP = 'Structure = heuristics; invariants = catalog+coverage evidence (not business runtime); ArkRun = kernel usage + declarations (not a score). Extra planes never merge into one architecture score. Advisory ArkRules ≠ merge teeth. Advisory ArkRun ≠ merge teeth.';
|
|
78
|
+
function countOrZero(value) {
|
|
79
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Which extra planes can fail merge. Counts and stamps only — never a score.
|
|
83
|
+
*/
|
|
84
|
+
export function composeMergePlanesHonesty(input = {}) {
|
|
85
|
+
const normalized = normalizeExtraMergeTeethClassification(input.classification);
|
|
86
|
+
const governedPercent = typeof normalized.governedPercent === 'number' ? normalized.governedPercent : null;
|
|
87
|
+
const populatedLayerCount = typeof normalized.populatedLayerCount === 'number' ? normalized.populatedLayerCount : null;
|
|
88
|
+
const classificationKnown = governedPercent != null || populatedLayerCount != null;
|
|
89
|
+
const classificationAllowsTeeth = extraMergeTeethAllowed(normalized);
|
|
90
|
+
const arkRules = input.arkRules;
|
|
91
|
+
const structureEnforced = countOrZero(arkRules?.structureEnforced);
|
|
92
|
+
const structureTotal = countOrZero(arkRules?.structureTotal);
|
|
93
|
+
const structureAdvisory = typeof arkRules?.structureAdvisory === 'number'
|
|
94
|
+
? countOrZero(arkRules.structureAdvisory)
|
|
95
|
+
: Math.max(0, structureTotal - structureEnforced);
|
|
96
|
+
const invariantEnforced = countOrZero(arkRules?.invariantEnforced);
|
|
97
|
+
const invariantTotal = countOrZero(arkRules?.invariantTotal);
|
|
98
|
+
const invariantAdvisory = typeof arkRules?.invariantAdvisory === 'number'
|
|
99
|
+
? countOrZero(arkRules.invariantAdvisory)
|
|
100
|
+
: Math.max(0, invariantTotal - invariantEnforced);
|
|
101
|
+
const arkRulesHasEnforced = structureEnforced > 0 || invariantEnforced > 0;
|
|
102
|
+
const arkRunPresent = input.arkRun?.present === true;
|
|
103
|
+
const arkRunMode = input.arkRun?.mode === 'enforced' || input.arkRun?.mode === 'advisory'
|
|
104
|
+
? input.arkRun.mode
|
|
105
|
+
: null;
|
|
106
|
+
const arkRunResidual = countOrZero(input.arkRun?.residualCount);
|
|
107
|
+
const arkRunHasEnforced = arkRunPresent && arkRunMode === 'enforced';
|
|
108
|
+
const arkRunTeeth = arkRunHasEnforced && classificationAllowsTeeth;
|
|
109
|
+
const hasEnforcedTeeth = arkRulesHasEnforced || arkRunHasEnforced;
|
|
110
|
+
const extraMergeTeeth = hasEnforcedTeeth && classificationAllowsTeeth;
|
|
111
|
+
const teethDeferredForClassification = hasEnforcedTeeth && classificationKnown && !classificationAllowsTeeth;
|
|
112
|
+
let failMergeWhen;
|
|
113
|
+
if (extraMergeTeeth) {
|
|
114
|
+
const extras = [];
|
|
115
|
+
if (arkRulesHasEnforced)
|
|
116
|
+
extras.push('enforced structure/invariant findings');
|
|
117
|
+
if (arkRunHasEnforced)
|
|
118
|
+
extras.push('enforced ArkRun skip findings');
|
|
119
|
+
failMergeWhen = `Layer graph failures plus ${extras.join(' and ')} (advisory extras never fail merge alone).`;
|
|
120
|
+
}
|
|
121
|
+
else if (teethDeferredForClassification) {
|
|
122
|
+
const which = [
|
|
123
|
+
arkRulesHasEnforced ? 'ArkRules structure/invariant' : null,
|
|
124
|
+
arkRunHasEnforced ? 'ArkRun' : null,
|
|
125
|
+
]
|
|
126
|
+
.filter((part) => Boolean(part))
|
|
127
|
+
.join(' and ');
|
|
128
|
+
failMergeWhen = `Layer graph only — enforced ${which} findings are demoted under the teeth floor (need ≥${EXTRA_MERGE_TEETH_GOVERNED_FLOOR}% governed and ≥1 populated layer); they do not merge-block until classification is honest.`;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
const arkRunBit = !arkRunPresent
|
|
132
|
+
? ' Absence of arkRun is silent.'
|
|
133
|
+
: arkRunMode === 'advisory'
|
|
134
|
+
? ' Advisory ArkRun never merge-blocks.'
|
|
135
|
+
: ' ArkRun extra is present but does not arm merge teeth.';
|
|
136
|
+
failMergeWhen =
|
|
137
|
+
'Layer graph only — no enforced ArkRules structure/invariant teeth on this tree. Advisory packs do not arm merge teeth.' +
|
|
138
|
+
arkRunBit;
|
|
139
|
+
}
|
|
140
|
+
const out = {
|
|
141
|
+
layers: {
|
|
142
|
+
role: 'inter-layer-edges',
|
|
143
|
+
alwaysOnGate: true,
|
|
144
|
+
note: 'Import/export layer graph — the default merge plane. Absent arkRules or arkRun changes nothing here.',
|
|
145
|
+
},
|
|
146
|
+
structureSensors: {
|
|
147
|
+
role: 'intra-layer-heuristics',
|
|
148
|
+
total: structureTotal,
|
|
149
|
+
enforced: structureEnforced,
|
|
150
|
+
advisory: structureAdvisory,
|
|
151
|
+
note: 'Structure sensors are heuristics (prefer false negatives). Only mode:enforced fails merge; noisy sensors stay advisory by default. Advisory-only packs never add merge teeth (FG-ARKRULES-ADVISORY-ONLY).',
|
|
152
|
+
},
|
|
153
|
+
invariants: {
|
|
154
|
+
role: 'catalog-plus-coverage',
|
|
155
|
+
total: invariantTotal,
|
|
156
|
+
enforced: invariantEnforced,
|
|
157
|
+
advisory: invariantAdvisory,
|
|
158
|
+
covered: countOrZero(arkRules?.covered),
|
|
159
|
+
uncovered: countOrZero(arkRules?.uncovered),
|
|
160
|
+
note: 'Invariants are catalog + coverage evidence, not a business runtime. Enforced + proven-uncovered fails merge; absence of enforced rules adds no extra teeth.',
|
|
161
|
+
},
|
|
162
|
+
arkRun: {
|
|
163
|
+
role: 'kernel-usage-and-declarations',
|
|
164
|
+
present: arkRunPresent,
|
|
165
|
+
mode: arkRunMode,
|
|
166
|
+
residualCount: arkRunResidual,
|
|
167
|
+
extraMergeTeeth: arkRunTeeth,
|
|
168
|
+
note: arkRunPresent
|
|
169
|
+
? arkRunMode === 'enforced'
|
|
170
|
+
? 'Enforced ArkRun arms extra merge teeth only when the layer plane is classified. Residual is a count, never a score.'
|
|
171
|
+
: 'Advisory ArkRun never adds merge teeth and never flips valid. Residual is a count, never a score.'
|
|
172
|
+
: 'Absence of arkRun is silent — Layers and ArkRules verdicts unchanged. The extra never becomes a score.',
|
|
173
|
+
},
|
|
174
|
+
extraMergeTeeth,
|
|
175
|
+
dualPlaneStamp: MERGE_PLANES_DUAL_STAMP,
|
|
176
|
+
failMergeWhen,
|
|
177
|
+
};
|
|
178
|
+
if (classificationKnown) {
|
|
179
|
+
out.classificationGate = {
|
|
180
|
+
governedPercent,
|
|
181
|
+
populatedLayerCount,
|
|
182
|
+
floorPercent: EXTRA_MERGE_TEETH_GOVERNED_FLOOR,
|
|
183
|
+
allowsTeeth: classificationAllowsTeeth,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
@@ -3,6 +3,19 @@ import { spawnSync } from 'node:child_process';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
|
+
/** Kill hung gh instead of stalling CI. */
|
|
7
|
+
export const SPAWN_TIMEOUT_MS = 8000;
|
|
8
|
+
|
|
9
|
+
function runGh(args, opts = {}) {
|
|
10
|
+
const { run, ...rest } = opts;
|
|
11
|
+
const spawn = typeof run === 'function' ? run : spawnSync;
|
|
12
|
+
return spawn('gh', args, {
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
...rest,
|
|
15
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
6
19
|
const IF_LINE = /^[ \t]*(?:-\s+)?(?:"if"|'if'|if):\s*(.*?)\s*(?:#.*)?$/i;
|
|
7
20
|
const CONTINUE_LINE = /^[ \t]*(?:-\s+)?(?:"continue-on-error"|'continue-on-error'|continue-on-error):\s*(.*?)\s*(?:#.*)?$/i;
|
|
8
21
|
const SAFE_IF = /^(?:['"]?true['"]?|['"]?\$\{\{\s*(?:true|always\(\))\s*\}\}['"]?)$/i;
|
|
@@ -434,7 +447,7 @@ export function reportGithubCiRuntime(opts = {}) {
|
|
|
434
447
|
const cwd = opts.cwd ?? process.cwd();
|
|
435
448
|
const env = opts.env ?? process.env;
|
|
436
449
|
const limit = Number.isFinite(Number(opts.limit)) ? Math.max(1, Number(opts.limit)) : 30;
|
|
437
|
-
if (
|
|
450
|
+
if (runGh(['--version'], { env, run: opts.run }).status !== 0) {
|
|
438
451
|
return { runtimeObserved: false, latestCiRun: null, reason: 'gh-cli-unavailable' };
|
|
439
452
|
}
|
|
440
453
|
const args = [
|
|
@@ -443,7 +456,7 @@ export function reportGithubCiRuntime(opts = {}) {
|
|
|
443
456
|
'--json', 'name,conclusion,status,workflowName,displayTitle,event',
|
|
444
457
|
];
|
|
445
458
|
if (opts.repo) args.push('--repo', opts.repo);
|
|
446
|
-
const result =
|
|
459
|
+
const result = runGh(args, { cwd, env, run: opts.run });
|
|
447
460
|
if (result.status !== 0) {
|
|
448
461
|
const err = `${result.stderr || ''}${result.stdout || ''}`.slice(0, 400);
|
|
449
462
|
return {
|
|
@@ -499,14 +512,14 @@ export function reportGithubCiRuntime(opts = {}) {
|
|
|
499
512
|
export function reportGithubBranchProtection(opts = {}) {
|
|
500
513
|
const cwd = opts.cwd ?? process.cwd();
|
|
501
514
|
const env = opts.env ?? process.env;
|
|
502
|
-
if (
|
|
515
|
+
if (runGh(['--version'], { env, run: opts.run }).status !== 0) {
|
|
503
516
|
return { available: false, reason: 'gh-cli-unavailable', runtimeObserved: false, latestCiRun: null };
|
|
504
517
|
}
|
|
505
518
|
let repo = opts.repo;
|
|
506
519
|
let branch = opts.branch;
|
|
507
520
|
if (!repo || !branch) {
|
|
508
521
|
const args = ['repo', 'view', ...(repo ? [repo] : []), '--json', 'nameWithOwner,defaultBranchRef'];
|
|
509
|
-
const metadata = parseJson(
|
|
522
|
+
const metadata = parseJson(runGh(args, { cwd, env, run: opts.run }));
|
|
510
523
|
if (!metadata?.nameWithOwner || !metadata?.defaultBranchRef?.name) {
|
|
511
524
|
return { available: false, reason: 'gh-repo-unavailable', runtimeObserved: false, latestCiRun: null };
|
|
512
525
|
}
|
|
@@ -514,13 +527,13 @@ export function reportGithubBranchProtection(opts = {}) {
|
|
|
514
527
|
branch ??= metadata.defaultBranchRef.name;
|
|
515
528
|
}
|
|
516
529
|
|
|
517
|
-
const classicResult =
|
|
530
|
+
const classicResult = runGh([
|
|
518
531
|
'api', `repos/${repo}/branches/${encodeURIComponent(branch)}/protection`, '--jq',
|
|
519
532
|
'{strict: .required_status_checks.strict, contexts: .required_status_checks.contexts, checks: .required_status_checks.checks, enforcesAdmins: .enforce_admins.enabled}',
|
|
520
|
-
], { cwd,
|
|
521
|
-
const rulesResult =
|
|
522
|
-
|
|
523
|
-
{ cwd,
|
|
533
|
+
], { cwd, env, run: opts.run });
|
|
534
|
+
const rulesResult = runGh(
|
|
535
|
+
['api', `repos/${repo}/rules/branches/${encodeURIComponent(branch)}`],
|
|
536
|
+
{ cwd, env, run: opts.run }
|
|
524
537
|
);
|
|
525
538
|
const classic = parseJson(classicResult);
|
|
526
539
|
const rules = parseJson(rulesResult);
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { effectiveCapabilityDeny } from './analysis-engine.mjs';
|
|
11
11
|
import { graphBlindSpotsHtml } from './graph-blind.mjs';
|
|
12
12
|
import { formatRulesUnderContractHtml } from './rules-under-contract.mjs';
|
|
13
|
+
import { formatArkRunHtml } from './ark-run-report.mjs';
|
|
13
14
|
import { primaryImprovementCompassNextAction } from './improvement-compass.mjs';
|
|
14
15
|
|
|
15
16
|
// htmlEscape is injected by the caller (html-report.mjs) — importing it back
|
|
@@ -351,6 +352,7 @@ export function renderAdvisorySections(advisories, escape) {
|
|
|
351
352
|
parseHealthHtml(advisories.parseHealth),
|
|
352
353
|
graphBlindSpotsHtml(advisories.graphBlindSpots, esc),
|
|
353
354
|
rulesUnderContractHtml(advisories.rulesUnderContract),
|
|
355
|
+
formatArkRunHtml(advisories.arkRun, esc),
|
|
354
356
|
]
|
|
355
357
|
.filter(Boolean)
|
|
356
358
|
.join('\n');
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
buildProductHonesty,
|
|
20
20
|
} from './enforcement-honesty.mjs';
|
|
21
21
|
import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
|
|
22
|
+
import { summarizeArkRunSection } from './ark-run-doctor.mjs';
|
|
22
23
|
import { readBaseline, baselineOccurrenceKeys } from './violations.mjs';
|
|
23
24
|
import { describePackageVersionDualTruth } from './field-install.mjs';
|
|
24
25
|
import { buildDoctorImprovementCompass } from './improvement-compass-doctor.mjs';
|
|
@@ -127,13 +128,31 @@ export function buildReportDepthPayload(
|
|
|
127
128
|
packageVersionTruth?.selfHost === true ||
|
|
128
129
|
packageVersionTruth?.code === 'PACKAGE_PIN_SELF_HOST',
|
|
129
130
|
});
|
|
130
|
-
const
|
|
131
|
+
const classification = {
|
|
131
132
|
governedPercent: coverage?.governed?.percent ?? null,
|
|
132
133
|
populatedLayerCount: Array.isArray(coverage?.layers)
|
|
133
134
|
? coverage.layers.filter((row) => (row?.files ?? 0) > 0).length
|
|
134
135
|
: null,
|
|
135
136
|
classifiedFiles: coverage?.governed?.classifiedFiles ?? null,
|
|
137
|
+
};
|
|
138
|
+
const rulesUnderContract = summarizeRulesUnderContract(root, config, undefined, classification);
|
|
139
|
+
const arkRun = summarizeArkRunSection({
|
|
140
|
+
arkRun: config?.arkRun,
|
|
141
|
+
findings: activeViolations,
|
|
142
|
+
classification,
|
|
143
|
+
arkRules: {
|
|
144
|
+
active: rulesUnderContract?.active === true,
|
|
145
|
+
structureEnforced: rulesUnderContract?.mergePlanes?.structureSensors?.enforced,
|
|
146
|
+
structureTotal: rulesUnderContract?.mergePlanes?.structureSensors?.total,
|
|
147
|
+
structureAdvisory: rulesUnderContract?.mergePlanes?.structureSensors?.advisory,
|
|
148
|
+
invariantEnforced: rulesUnderContract?.mergePlanes?.invariants?.enforced,
|
|
149
|
+
invariantTotal: rulesUnderContract?.mergePlanes?.invariants?.total,
|
|
150
|
+
invariantAdvisory: rulesUnderContract?.mergePlanes?.invariants?.advisory,
|
|
151
|
+
covered: rulesUnderContract?.mergePlanes?.invariants?.covered,
|
|
152
|
+
uncovered: rulesUnderContract?.mergePlanes?.invariants?.uncovered,
|
|
153
|
+
},
|
|
136
154
|
});
|
|
155
|
+
if (rulesUnderContract?.mergePlanes) rulesUnderContract.mergePlanes = arkRun.mergePlanes;
|
|
137
156
|
// Single residual expression (parity with doctor): nextPilot || extractionCard.
|
|
138
157
|
const residualPilot =
|
|
139
158
|
pilotLoop?.nextPilot || pilotLoop?.extractionCard || null;
|
|
@@ -155,7 +174,7 @@ export function buildReportDepthPayload(
|
|
|
155
174
|
residualPilots: Boolean(residualPilot) && designFitness.designWeak === true,
|
|
156
175
|
pilotTarget: residualPilot?.pilotTarget ?? residualPilot?.pilot ?? null,
|
|
157
176
|
arkRulesMergeHonesty: rulesUnderContract?.mergePlanes
|
|
158
|
-
? { active: rulesUnderContract.active === true, ...rulesUnderContract.mergePlanes }
|
|
177
|
+
? { active: rulesUnderContract.active === true || arkRun?.active === true, ...rulesUnderContract.mergePlanes }
|
|
159
178
|
: null,
|
|
160
179
|
primaryNextAction: postGreenPath?.action ?? dualTruthNext,
|
|
161
180
|
activeBlockingViolations: activeBlockingCount,
|
|
@@ -198,6 +217,7 @@ export function buildReportDepthPayload(
|
|
|
198
217
|
// P0-B / P1-M — folded into designDepth so --report stays a single payload.
|
|
199
218
|
productHonesty,
|
|
200
219
|
mergePlanes: rulesUnderContract?.mergePlanes ?? null,
|
|
220
|
+
arkRun,
|
|
201
221
|
improvementCompass,
|
|
202
222
|
deepModuleCoach,
|
|
203
223
|
},
|
package/bin/lib/html-report.mjs
CHANGED
|
@@ -28,6 +28,9 @@ import { captureGitSnapshot } from './report-snapshot-context.mjs';
|
|
|
28
28
|
|
|
29
29
|
export { arkGitignoreAppendDecision, gitignoreCoversArkState, gitignoreHasArkNegationException } from './ark-gitignore.mjs';
|
|
30
30
|
|
|
31
|
+
/** Cap the rendered violation list so showcase HTML cannot dump unbounded findings. */
|
|
32
|
+
export const HTML_REPORT_VIOLATION_LIST_CAP = 12;
|
|
33
|
+
|
|
31
34
|
export function detectEnforcement(root) {
|
|
32
35
|
const has = (rel) => fs.existsSync(path.join(root, rel));
|
|
33
36
|
const fileIncludes = (rel, needle) => {
|
|
@@ -146,6 +149,8 @@ export function buildReportSnapshot({
|
|
|
146
149
|
mode,
|
|
147
150
|
/** DF02 — thin status compass slice (mode + residual ids, notAScore). */
|
|
148
151
|
improvementCompass = null,
|
|
152
|
+
/** RN08 — thin status ArkRun slice (notAScore; residual count, never a score). */
|
|
153
|
+
arkRun = null,
|
|
149
154
|
}) {
|
|
150
155
|
const layers = Array.isArray(config?.layers) ? config.layers : [];
|
|
151
156
|
const rules = Array.isArray(config?.rules) ? config.rules : [];
|
|
@@ -193,6 +198,20 @@ export function buildReportSnapshot({
|
|
|
193
198
|
if (improvementCompass && typeof improvementCompass === 'object') {
|
|
194
199
|
snapshot.improvementCompass = improvementCompass;
|
|
195
200
|
}
|
|
201
|
+
if (arkRun && typeof arkRun === 'object' && arkRun.notAScore === true) {
|
|
202
|
+
snapshot.arkRun = {
|
|
203
|
+
notAScore: true,
|
|
204
|
+
present: arkRun.active === true || arkRun.present === true,
|
|
205
|
+
mode: arkRun.mode === 'enforced' || arkRun.mode === 'advisory' ? arkRun.mode : null,
|
|
206
|
+
extraMergeTeeth: arkRun.extraMergeTeeth === true,
|
|
207
|
+
residual:
|
|
208
|
+
typeof arkRun.residual === 'number'
|
|
209
|
+
? arkRun.residual
|
|
210
|
+
: typeof arkRun.residual?.count === 'number'
|
|
211
|
+
? arkRun.residual.count
|
|
212
|
+
: null,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
196
215
|
return snapshot;
|
|
197
216
|
}
|
|
198
217
|
|
|
@@ -392,9 +411,13 @@ export function renderBeginnerHtmlReport({ root, config, violations, ok, version
|
|
|
392
411
|
})
|
|
393
412
|
.join('\n');
|
|
394
413
|
|
|
414
|
+
const listedBeginner = violations.slice(0, HTML_REPORT_VIOLATION_LIST_CAP);
|
|
415
|
+
const hiddenBeginner = Math.max(0, violations.length - listedBeginner.length);
|
|
416
|
+
const beginnerRemainder = hiddenBeginner
|
|
417
|
+
? `<div class="dim">+${hiddenBeginner} more (${violations.length} total)</div>`
|
|
418
|
+
: '';
|
|
395
419
|
const violationRows = violations.length
|
|
396
|
-
?
|
|
397
|
-
.slice(0, 12)
|
|
420
|
+
? listedBeginner
|
|
398
421
|
.map((v) => {
|
|
399
422
|
const enriched = enrichViolationWithFixClass(v);
|
|
400
423
|
return `<li><code>${esc(v.file)}:${v.line}</code> — ${esc(enriched.enthusiastHint ?? v.message)}</li>`;
|
|
@@ -436,6 +459,7 @@ export function renderBeginnerHtmlReport({ root, config, violations, ok, version
|
|
|
436
459
|
</table>
|
|
437
460
|
<h2>What to fix first</h2>
|
|
438
461
|
<ul>${violationRows}</ul>
|
|
462
|
+
${beginnerRemainder}
|
|
439
463
|
<h2>Next steps</h2>
|
|
440
464
|
<p><code>${arkCheckCommand(root)}</code></p>
|
|
441
465
|
<p><code>${arkCommand(root, 'ark-check', '--recommend')}</code></p>
|
|
@@ -781,13 +805,22 @@ export function renderHtmlReport({
|
|
|
781
805
|
})
|
|
782
806
|
.join('\n');
|
|
783
807
|
|
|
784
|
-
const
|
|
808
|
+
const listedViolations = violations.slice(0, HTML_REPORT_VIOLATION_LIST_CAP);
|
|
809
|
+
const hiddenViolationCount = Math.max(0, violations.length - listedViolations.length);
|
|
810
|
+
const ruleTotals = new Map();
|
|
785
811
|
for (const v of violations) {
|
|
812
|
+
ruleTotals.set(v.ruleId, (ruleTotals.get(v.ruleId) || 0) + 1);
|
|
813
|
+
}
|
|
814
|
+
const byRule = new Map();
|
|
815
|
+
for (const v of listedViolations) {
|
|
786
816
|
if (!byRule.has(v.ruleId)) byRule.set(v.ruleId, []);
|
|
787
817
|
byRule.get(v.ruleId).push(v);
|
|
788
818
|
}
|
|
789
|
-
const
|
|
790
|
-
?
|
|
819
|
+
const remainderNote = hiddenViolationCount
|
|
820
|
+
? `<div class="dim">+${hiddenViolationCount} more (${violations.length} total)</div>`
|
|
821
|
+
: '';
|
|
822
|
+
const violationBlocks = listedViolations.length
|
|
823
|
+
? `${[...byRule.entries()]
|
|
791
824
|
.map(([ruleId, items]) => {
|
|
792
825
|
const hint = FIX_HINTS[ruleId];
|
|
793
826
|
const rows = items
|
|
@@ -803,12 +836,12 @@ export function renderHtmlReport({
|
|
|
803
836
|
})
|
|
804
837
|
.join('\n');
|
|
805
838
|
return `<div class="vgroup">
|
|
806
|
-
<div class="vghead"><span class="rule">${esc(ruleId)}</span> <span class="dim">${items.length}</span></div>
|
|
839
|
+
<div class="vghead"><span class="rule">${esc(ruleId)}</span> <span class="dim">${ruleTotals.get(ruleId) ?? items.length}</span></div>
|
|
807
840
|
<ul class="vitems">${rows}</ul>
|
|
808
841
|
${hint ? `<div class="fix">fix: ${esc(hint)}</div>` : ''}
|
|
809
842
|
</div>`;
|
|
810
843
|
})
|
|
811
|
-
.join('\n')
|
|
844
|
+
.join('\n')}${remainderNote}`
|
|
812
845
|
: `<div class="clean hero-clean">
|
|
813
846
|
<div class="clean-title">Architecture matches the contract</div>
|
|
814
847
|
<div class="clean-body">No active violations${suppressed ? ` · ${suppressed} frozen by baseline` : ''}. This is what “honest green” looks like when coverage is real.</div>
|