arkgate 4.8.13 → 4.8.15
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 +219 -4
- package/README.md +33 -22
- package/SECURITY.md +5 -3
- package/bin/ark-check-runtime.mjs +25 -57
- package/bin/ark-mcp-runtime.mjs +111 -2
- package/bin/ark-shared.mjs +140 -11
- package/bin/ark.mjs +7 -25
- package/bin/lib/adr-presence.mjs +97 -0
- package/bin/lib/agent-projection-formatters.mjs +2 -0
- package/bin/lib/agent-skills-package.mjs +241 -8
- package/bin/lib/analysis-completeness.mjs +38 -2
- package/bin/lib/analysis-engine.mjs +6 -6
- package/bin/lib/architecture-scan.mjs +33 -5
- package/bin/lib/ark-order-doctor.mjs +7 -1
- package/bin/lib/ark-order-report.mjs +2 -1
- package/bin/lib/ark-run-doctor.mjs +6 -0
- package/bin/lib/ark-run-report.mjs +6 -2
- package/bin/lib/arkrules-sensors.mjs +81 -0
- package/bin/lib/baseline-key.mjs +4 -1
- package/bin/lib/check-args.mjs +52 -2
- package/bin/lib/ci-and-commands.mjs +7 -2
- package/bin/lib/config-contract.mjs +64 -1
- package/bin/lib/design-smells.mjs +21 -1
- package/bin/lib/diagnostic-catalog.mjs +5 -3
- package/bin/lib/doctor-advisories.mjs +109 -17
- package/bin/lib/doctor-human.mjs +20 -6
- package/bin/lib/doctor-next-actions.mjs +14 -0
- package/bin/lib/doctor-plan.mjs +32 -2
- package/bin/lib/enforcement-honesty.mjs +47 -2
- package/bin/lib/first-run-help.mjs +19 -3
- package/bin/lib/gate-files.mjs +40 -3
- package/bin/lib/host-support-matrix.mjs +9 -1
- package/bin/lib/html-report-depth.mjs +2 -0
- package/bin/lib/html-report.mjs +13 -2
- package/bin/lib/import-resolve.mjs +74 -13
- package/bin/lib/improvement-compass-doctor.mjs +6 -1
- package/bin/lib/improvement-compass-map.mjs +3 -1
- package/bin/lib/install-migrate.mjs +23 -0
- package/bin/lib/layer-description.mjs +90 -8
- package/bin/lib/mcp-hook-payload.mjs +57 -1
- package/bin/lib/no-domain-frontend.mjs +91 -0
- package/bin/lib/presets.mjs +3 -4
- package/bin/lib/product-copy.mjs +19 -0
- package/bin/lib/projected-governed-coverage.mjs +114 -0
- package/bin/lib/recommend-cli.mjs +54 -0
- package/bin/lib/remediation.mjs +6 -2
- package/bin/lib/resolved-candidate-facts.mjs +225 -100
- package/bin/lib/rules-under-contract.mjs +117 -2
- package/bin/lib/scan-files.mjs +39 -0
- package/bin/lib/snippet-analysis.mjs +14 -8
- package/bin/lib/start-preview.mjs +8 -2
- package/bin/lib/states-transitions-presence.mjs +212 -0
- package/bin/lib/status-command.mjs +2 -0
- package/bin/lib/status-transition-catalog.mjs +410 -0
- package/bin/lib/team-parliament-io.mjs +10 -0
- package/bin/lib/upgrade-whats-new.mjs +3 -3
- package/bin/lib/violations.mjs +43 -1
- package/bin/lib/write-path-capabilities.mjs +20 -5
- package/bin/lib/write-path-detect.mjs +27 -2
- package/dist/{configTypes-j7so8B4O.d.ts → configTypes-Dt3DpVbd.d.ts} +19 -0
- package/dist/{diagnosticCatalog-DA565Lja.d.ts → diagnosticCatalog-BEg85XlE.d.ts} +3 -3
- package/dist/eslint/index.cjs +4 -4
- package/dist/eslint/index.d.ts +1 -1
- package/dist/eslint/index.js +4 -4
- package/dist/index.cjs +31 -31
- package/dist/index.d.ts +126 -19
- package/dist/index.js +31 -31
- package/dist/nestjs/index.cjs +5 -5
- package/dist/nestjs/index.d.ts +3 -3
- package/dist/nestjs/index.js +5 -5
- package/dist/runtime/index.cjs +15 -15
- package/dist/runtime/index.d.ts +6 -6
- package/dist/runtime/index.js +15 -15
- package/dist/{types-Djbs3KjE.d.ts → types-CN9tVMPz.d.ts} +3 -1
- package/dist/{types-tGhZUiGX.d.ts → types-TBiv0WHL.d.ts} +1 -1
- package/docs/README.md +8 -6
- package/docs/agent-guide.md +35 -17
- package/docs/ai-gates.md +13 -4
- package/docs/arkorder.md +11 -4
- package/docs/brownfield-adoption.md +4 -1
- package/docs/configuration.md +63 -14
- package/docs/develop.md +4 -1
- package/docs/diagnostics.md +30 -10
- package/docs/enthusiast/README.md +6 -1
- package/docs/enthusiast/how-to-agent-gates.md +5 -0
- package/docs/enthusiast/how-to-gallery-starter.md +2 -1
- package/docs/enthusiast/how-to-pick-shape.md +1 -1
- package/docs/package-surface.md +10 -6
- package/docs/product-voice.md +32 -6
- package/docs/threat-model.md +2 -2
- package/docs/typescript-support.md +3 -3
- package/docs/use.md +23 -11
- package/package.json +1 -1
- package/schemas/ark.config.schema.json +23 -2
- package/server.json +2 -2
- package/templates/agent-skills/README.md +7 -4
- package/templates/agent-skills/ark-adopt/SKILL.md +91 -6
- package/templates/agent-skills/ark-architect/SKILL.md +5 -18
- package/templates/agent-skills/ark-autopilot/SKILL.md +14 -6
- package/templates/agent-skills/ark-contract/SKILL.md +9 -20
- package/templates/agent-skills/ark-coverage/SKILL.md +13 -9
- package/templates/agent-skills/ark-explain/SKILL.md +11 -5
- package/templates/agent-skills/ark-explore/SKILL.md +39 -5
- package/templates/agent-skills/ark-fix/SKILL.md +15 -20
- package/templates/agent-skills/ark-loop/SKILL.md +14 -20
- package/templates/agent-skills/ark-order/SKILL.md +200 -0
- package/templates/agent-skills/ark-place/SKILL.md +49 -11
- package/templates/agent-skills/ark-runtime/SKILL.md +21 -6
- package/templates/agent-skills/ark-think/SKILL.md +24 -126
- package/templates/agent-skills/ark-upgrade/SKILL.md +14 -3
- package/templates/arkrules/DomainModel.json +14 -1
- package/templates/skills/ark-adopt.md +91 -6
- package/templates/skills/ark-architect.md +5 -18
- package/templates/skills/ark-autopilot.md +14 -6
- package/templates/skills/ark-contract.md +9 -20
- package/templates/skills/ark-coverage.md +13 -9
- package/templates/skills/ark-explain.md +11 -5
- package/templates/skills/ark-explore.md +39 -5
- package/templates/skills/ark-fix.md +15 -20
- package/templates/skills/ark-loop.md +14 -20
- package/templates/skills/ark-order.md +200 -0
- package/templates/skills/ark-place.md +49 -11
- package/templates/skills/ark-runtime.md +21 -6
- package/templates/skills/ark-think.md +24 -126
- package/templates/skills/ark-upgrade.md +14 -3
- package/templates/tests/ark-adoption-gaps.test.ts +5 -4
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
loadInvariantCoverageInputs,
|
|
18
18
|
} from './invariant-coverage-io.mjs';
|
|
19
19
|
import { loadArkRuleFileHints } from './arkrule-file-hints.mjs';
|
|
20
|
+
import { collectGovernedFiles } from './scan-files.mjs';
|
|
20
21
|
|
|
21
22
|
const HINT_CACHE_CAP = 16;
|
|
22
23
|
/** Process-local hint map keyed by scoped path + content hash. Not a second engine. */
|
|
@@ -40,15 +41,34 @@ function normalizeScanRelPath(root, filePath) {
|
|
|
40
41
|
return relative;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Empty / missing `files` stays unbounded (full governed set).
|
|
46
|
+
* A complete governed list is also unbounded — doctor and check always pass
|
|
47
|
+
* that list, and treating it as `--changed` paid import-closure + a second
|
|
48
|
+
* `resolveModuleName` pass on every full-tree run (#212).
|
|
49
|
+
*/
|
|
50
|
+
function fileLocalScope(root, files, config, { changed = false } = {}) {
|
|
45
51
|
if (!Array.isArray(files) || files.length === 0) return null;
|
|
46
52
|
const scoped = new Set();
|
|
47
53
|
for (const file of files) {
|
|
48
54
|
const rel = normalizeScanRelPath(root, file);
|
|
49
55
|
if (rel) scoped.add(rel);
|
|
50
56
|
}
|
|
51
|
-
|
|
57
|
+
if (scoped.size === 0) return null;
|
|
58
|
+
// `--changed` is already a bounded envelope — do not re-walk the include tree (#205).
|
|
59
|
+
if (!changed && config && coversGovernedSet(root, scoped, config)) return null;
|
|
60
|
+
return scoped;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function coversGovernedSet(root, scoped, config) {
|
|
64
|
+
const governed = collectGovernedFiles(root, config);
|
|
65
|
+
if (governed.length === 0) return false;
|
|
66
|
+
if (scoped.size < governed.length) return false;
|
|
67
|
+
for (const absolute of governed) {
|
|
68
|
+
const rel = normalizeScanRelPath(root, absolute);
|
|
69
|
+
if (rel && !scoped.has(rel)) return false;
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
52
72
|
}
|
|
53
73
|
|
|
54
74
|
function filterHintPreload(fileContents, scoped) {
|
|
@@ -126,12 +146,20 @@ export function resolveArchitectureSnapshot({
|
|
|
126
146
|
{ ...config, rules: rules ?? config.rules },
|
|
127
147
|
manifest
|
|
128
148
|
);
|
|
149
|
+
const scoped = fileLocalScope(root, files, effectiveConfig, {
|
|
150
|
+
changed: args?.changed === true,
|
|
151
|
+
});
|
|
129
152
|
const facts = resolveCandidateFacts({
|
|
130
153
|
root,
|
|
131
154
|
config: effectiveConfig,
|
|
132
155
|
ts,
|
|
133
156
|
...(args?.tsconfig ? { tsconfig: args.tsconfig } : {}),
|
|
134
157
|
observeInput,
|
|
158
|
+
...(scoped
|
|
159
|
+
? { scopeFiles: [...scoped] }
|
|
160
|
+
: args?.changed
|
|
161
|
+
? { scopeFiles: [] }
|
|
162
|
+
: {}),
|
|
135
163
|
});
|
|
136
164
|
const arkRulesLoad = loadEffectiveArkRulesFromDisk(root, effectiveConfig, {
|
|
137
165
|
observeInput,
|
|
@@ -145,7 +173,6 @@ export function resolveArchitectureSnapshot({
|
|
|
145
173
|
err.issues = arkRulesLoad.errors;
|
|
146
174
|
throw err;
|
|
147
175
|
}
|
|
148
|
-
const scoped = fileLocalScope(root, files);
|
|
149
176
|
const loadedContract = loadContract(effectiveConfig, configPath, {
|
|
150
177
|
arkRules: arkRulesLoad.arkRules,
|
|
151
178
|
});
|
|
@@ -162,7 +189,8 @@ export function resolveArchitectureSnapshot({
|
|
|
162
189
|
...coverageOptionsFromConfig(effectiveConfig),
|
|
163
190
|
})
|
|
164
191
|
: undefined;
|
|
165
|
-
// File-local
|
|
192
|
+
// File-local sensors honor the touched set. Facts (and graph) use that set plus
|
|
193
|
+
// its import closure — not the whole include tree. Unbounded `files` stays full-tree.
|
|
166
194
|
const fileHints = loadHintsForScope(
|
|
167
195
|
root,
|
|
168
196
|
facts,
|
|
@@ -12,6 +12,8 @@ import { composeMergePlanesHonesty, extraMergeTeethAllowed, isArkOrderRuleId, }
|
|
|
12
12
|
export const ARK_ORDER_DOCTOR_SCHEMA_VERSION = '1.0';
|
|
13
13
|
const RESIDUAL_RULE_CAP = 12;
|
|
14
14
|
export const ARKORDER_ONE_BREATH = 'Layers stop a bad import. ArkOrder stops rewriting a big product choice — like the billing plan — as if it were a seat count. Change those choices through a valve, not a generic update.';
|
|
15
|
+
/** First-contact next step. Reuses the billing gallery + existing doors. */
|
|
16
|
+
export const ARKORDER_FIRST_CONTACT_NEXT = 'Next: examples/arkorder-billing, then /ark-adopt to turn the extra on and /ark-order to wire one candidate.';
|
|
15
17
|
function closedMode(value) {
|
|
16
18
|
return value === 'enforced' || value === 'advisory' ? value : null;
|
|
17
19
|
}
|
|
@@ -129,7 +131,11 @@ export function formatArkOrderDoctorLines(section) {
|
|
|
129
131
|
if (!section || section.notAScore !== true)
|
|
130
132
|
return [];
|
|
131
133
|
if (section.active !== true) {
|
|
132
|
-
return [
|
|
134
|
+
return [
|
|
135
|
+
ARKORDER_ONE_BREATH,
|
|
136
|
+
'Off — Layers stay the same (not a score).',
|
|
137
|
+
ARKORDER_FIRST_CONTACT_NEXT,
|
|
138
|
+
];
|
|
133
139
|
}
|
|
134
140
|
const mode = section.mode ?? 'unknown';
|
|
135
141
|
const teeth = section.extraMergeTeeth === true ? 'armed' : 'not armed';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTML for the doctor ArkOrder advisory (report parity: data-advisory="arkOrder").
|
|
3
3
|
*/
|
|
4
|
-
import { ARKORDER_ONE_BREATH } from './ark-order-doctor.mjs';
|
|
4
|
+
import { ARKORDER_FIRST_CONTACT_NEXT, ARKORDER_ONE_BREATH } from './ark-order-doctor.mjs';
|
|
5
5
|
|
|
6
6
|
export function formatArkOrderHtml(section, esc) {
|
|
7
7
|
if (!section || typeof section !== 'object' || section.notAScore !== true) return '';
|
|
@@ -14,6 +14,7 @@ export function formatArkOrderHtml(section, esc) {
|
|
|
14
14
|
<p class="dim" style="margin:.15rem 0 .55rem;font-size:.88rem">
|
|
15
15
|
${ARKORDER_ONE_BREATH}
|
|
16
16
|
Off until you turn it on — Layers stay the same.
|
|
17
|
+
${ARKORDER_FIRST_CONTACT_NEXT}
|
|
17
18
|
</p>
|
|
18
19
|
${note}
|
|
19
20
|
</section>`;
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
import { composeMergePlanesHonesty, extraMergeTeethAllowed, isArkRunRuleId, } from './extra-merge-teeth.mjs';
|
|
12
12
|
export const ARK_RUN_DOCTOR_SCHEMA_VERSION = '1.0';
|
|
13
13
|
const RESIDUAL_RULE_CAP = 12;
|
|
14
|
+
/** Compact / details — only when `arkRun` is on. Absence stays silent. */
|
|
15
|
+
export const ARKRUN_ONE_BREATH = 'Layers stop a bad import. ArkRun is an optional experimental runtime — in-memory, not Postgres. Data is gone on restart.';
|
|
16
|
+
/** Next step when the extra is on and residual remains. */
|
|
17
|
+
export const ARKRUN_FIRST_CONTACT_NEXT = 'Next: /ark-runtime to wire one candidate.';
|
|
14
18
|
function closedMode(value) {
|
|
15
19
|
return value === 'enforced' || value === 'advisory' ? value : null;
|
|
16
20
|
}
|
|
@@ -126,6 +130,7 @@ export function formatArkRunDoctorLines(section) {
|
|
|
126
130
|
const mode = section.mode ?? 'unknown';
|
|
127
131
|
const teeth = section.extraMergeTeeth === true ? 'armed' : 'not armed';
|
|
128
132
|
const lines = [
|
|
133
|
+
ARKRUN_ONE_BREATH,
|
|
129
134
|
`mode: ${mode} · extra merge teeth ${teeth} · not a score`,
|
|
130
135
|
];
|
|
131
136
|
if (section.residual.count > 0) {
|
|
@@ -134,6 +139,7 @@ export function formatArkRunDoctorLines(section) {
|
|
|
134
139
|
? ` (+${section.residual.count - section.residual.ruleIds.length} more)`
|
|
135
140
|
: '';
|
|
136
141
|
lines.push(`Residual: ${shown}${more}`);
|
|
142
|
+
lines.push(ARKRUN_FIRST_CONTACT_NEXT);
|
|
137
143
|
}
|
|
138
144
|
else {
|
|
139
145
|
lines.push('Residual: none on this scan (not a score — green extras ≠ finished kernel wiring).');
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTML for the doctor ArkRun advisory (report parity: data-advisory="arkRun").
|
|
3
3
|
*/
|
|
4
|
+
import { ARKRUN_FIRST_CONTACT_NEXT, ARKRUN_ONE_BREATH } from './ark-run-doctor.mjs';
|
|
5
|
+
|
|
4
6
|
export function formatArkRunHtml(section, esc) {
|
|
5
7
|
if (!section || typeof section !== 'object' || section.notAScore !== true) return '';
|
|
6
8
|
const escape = typeof esc === 'function' ? esc : (v) => String(v);
|
|
@@ -17,6 +19,7 @@ export function formatArkRunHtml(section, esc) {
|
|
|
17
19
|
}
|
|
18
20
|
const residual = section.residual && typeof section.residual === 'object' ? section.residual : { count: 0, ruleIds: [] };
|
|
19
21
|
const ids = Array.isArray(residual.ruleIds) ? residual.ruleIds : [];
|
|
22
|
+
const next = residual.count > 0 ? ` ${ARKRUN_FIRST_CONTACT_NEXT}` : '';
|
|
20
23
|
const residualHtml =
|
|
21
24
|
residual.count > 0
|
|
22
25
|
? `<p><span class="tag warn">residual</span> ${ids
|
|
@@ -40,8 +43,9 @@ export function formatArkRunHtml(section, esc) {
|
|
|
40
43
|
<section class="section card" data-advisory="arkRun">
|
|
41
44
|
<h2>ArkRun <span class="muted">(not a score)</span></h2>
|
|
42
45
|
<p class="dim" style="margin:.15rem 0 .55rem;font-size:.88rem">
|
|
43
|
-
<b>[ArkRun]</b>
|
|
44
|
-
|
|
46
|
+
<b>[ArkRun]</b> ${ARKRUN_ONE_BREATH}
|
|
47
|
+
Kernel usage + declarations — separate from <b>[Layer]</b> imports and <b>[ArkRules]</b> shape.
|
|
48
|
+
Advisory never flips <code>valid</code>. Enforced extra teeth only when the layer plane is classified.${next}
|
|
45
49
|
</p>
|
|
46
50
|
${merge}
|
|
47
51
|
<div class="kpis" style="margin-bottom:.55rem">
|
|
@@ -378,6 +378,87 @@ export function collectEmptyAppliesToFindings(arkRules, files) {
|
|
|
378
378
|
a.arkruleId.localeCompare(b.arkruleId) ||
|
|
379
379
|
a.message.localeCompare(b.message));
|
|
380
380
|
}
|
|
381
|
+
/** Intent prefixes are short tokens (`Domain.`, `Application.`). Bound the scan. */
|
|
382
|
+
const INTENT_PREFIX_SCAN_LIMIT = 64;
|
|
383
|
+
/**
|
|
384
|
+
* Strip trailing `.` without a regex. `\.+$` on config strings is CodeQL
|
|
385
|
+
* js/polynomial-redos (backtracking on long runs of `.`). Linear walk is enough.
|
|
386
|
+
*/
|
|
387
|
+
function stripTrailingDots(value) {
|
|
388
|
+
let end = value.length;
|
|
389
|
+
while (end > 0 && value.charCodeAt(end - 1) === 46) {
|
|
390
|
+
end -= 1;
|
|
391
|
+
}
|
|
392
|
+
return end === value.length ? value : value.slice(0, end);
|
|
393
|
+
}
|
|
394
|
+
function normalizeIntentPrefix(prefix) {
|
|
395
|
+
const trimmed = prefix.trim();
|
|
396
|
+
const bounded = trimmed.length > INTENT_PREFIX_SCAN_LIMIT
|
|
397
|
+
? trimmed.slice(0, INTENT_PREFIX_SCAN_LIMIT)
|
|
398
|
+
: trimmed;
|
|
399
|
+
return stripTrailingDots(bounded);
|
|
400
|
+
}
|
|
401
|
+
function layerNameLooksDomain(layer) {
|
|
402
|
+
const lower = layer.toLowerCase();
|
|
403
|
+
return (lower.includes('domain') ||
|
|
404
|
+
lower.includes('entity') ||
|
|
405
|
+
lower.includes('aggregate') ||
|
|
406
|
+
lower.includes('model'));
|
|
407
|
+
}
|
|
408
|
+
function ownsDomainIntent(intentPrefixes) {
|
|
409
|
+
return intentPrefixes.some((prefix) => {
|
|
410
|
+
const normalized = normalizeIntentPrefix(prefix);
|
|
411
|
+
return normalized === 'Domain' || normalized.startsWith('Domain.');
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
/** Domain-role house: name or intentPrefixes. Keep aligned with rulesInventory. */
|
|
415
|
+
export function isDomainRoleLayerName(layer, intentPrefixes = []) {
|
|
416
|
+
return layerNameLooksDomain(layer) || ownsDomainIntent(intentPrefixes);
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* §2 catalog residual — ArkRules is on, Domain has code, invariants[] is empty.
|
|
420
|
+
* Advisory by default. failsStrict only when a domain structure rule is already
|
|
421
|
+
* enforced (the project opted into ArkRules merge teeth). Not freezable.
|
|
422
|
+
*/
|
|
423
|
+
export function collectEmptyInvariantCatalogFindings(input) {
|
|
424
|
+
if (input.arkRulesActive !== true)
|
|
425
|
+
return [];
|
|
426
|
+
const domainLayers = (input.layers ?? [])
|
|
427
|
+
.filter((layer) => isDomainRoleLayerName(layer.name, layer.intentPrefixes ?? []))
|
|
428
|
+
.map((layer) => layer.name);
|
|
429
|
+
if (domainLayers.length === 0)
|
|
430
|
+
return [];
|
|
431
|
+
const domainSet = new Set(domainLayers);
|
|
432
|
+
const domainPopulated = (input.files ?? []).some((file) => {
|
|
433
|
+
const layer = typeof file.layer === 'string' ? file.layer : '';
|
|
434
|
+
return layer.length > 0 && domainSet.has(layer);
|
|
435
|
+
});
|
|
436
|
+
if (!domainPopulated)
|
|
437
|
+
return [];
|
|
438
|
+
const domainInvariants = (input.arkRules.invariants ?? []).filter((inv) => domainSet.has(inv.provenance.layer));
|
|
439
|
+
if (domainInvariants.length > 0)
|
|
440
|
+
return [];
|
|
441
|
+
const fillLayer = domainLayers.find((name) => input.arkRules.byLayer?.[name]) ?? domainLayers[0];
|
|
442
|
+
const fillPath = input.arkRules.byLayer?.[fillLayer]?.sourceFile ?? `arkrules/${fillLayer}.json`;
|
|
443
|
+
const optedIntoTeeth = (input.arkRules.structure ?? []).some((rule) => rule.mode === 'enforced' &&
|
|
444
|
+
domainSet.has(rule.provenance.layer) &&
|
|
445
|
+
!isTier2(rule.sensor));
|
|
446
|
+
return [
|
|
447
|
+
{
|
|
448
|
+
ruleId: 'INVARIANT_CATALOG_EMPTY',
|
|
449
|
+
code: 'invariant-catalog-empty',
|
|
450
|
+
message: `ArkRules is on and ${fillLayer} has code, but invariants[] is empty — there are no phrases the code must preserve. Add 1–2 short phrases in ${fillPath}.`,
|
|
451
|
+
file: fillPath,
|
|
452
|
+
line: 1,
|
|
453
|
+
fromLayer: fillLayer,
|
|
454
|
+
arkruleId: 'invariant-catalog',
|
|
455
|
+
arkruleSource: fillPath,
|
|
456
|
+
severity: optedIntoTeeth ? 'error' : 'warning',
|
|
457
|
+
sensor: 'invariant-coverage',
|
|
458
|
+
failsStrict: optedIntoTeeth,
|
|
459
|
+
},
|
|
460
|
+
];
|
|
461
|
+
}
|
|
381
462
|
/** IO / ORM import evidence. postgres and drizzle-orm include package subpaths. Keep in lockstep with arkOrderFacts. */
|
|
382
463
|
const IO_IMPORT_HINT_RE = /\bfrom\s+['"](?:@?prisma\/client|@supabase\/|drizzle-orm(?:\/[^'"]+)?|postgres(?:\/[^'"]+)?|typeorm|knex|mongodb|pg|mysql2|mongoose|better-sqlite3|ioredis|redis|kysely|sequelize)['"]|require\(\s*['"](?:@?prisma\/client|pg|postgres(?:\/[^'"]+)?|drizzle-orm(?:\/[^'"]+)?|knex|typeorm|mongoose)/;
|
|
383
464
|
/**
|
package/bin/lib/baseline-key.mjs
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
/** Config diagnostics that are never code debt. Not a schema key. */
|
|
12
|
-
export const NON_FREEZABLE_BASELINE_RULE_IDS = [
|
|
12
|
+
export const NON_FREEZABLE_BASELINE_RULE_IDS = [
|
|
13
|
+
'ARKRULE_SCOPE_EMPTY',
|
|
14
|
+
'INVARIANT_CATALOG_EMPTY',
|
|
15
|
+
];
|
|
13
16
|
/**
|
|
14
17
|
* STRUCTURE freeze `target`: sensor id, plus `:symbol` when a method/class is known.
|
|
15
18
|
* V1 empty-target keys (`ARKRULE_STRUCTURE|file|layer||`) stay exact-match only —
|
package/bin/lib/check-args.mjs
CHANGED
|
@@ -14,7 +14,55 @@ export function resolveDesignDeltaBaseRef(root, explicit, env = process.env) {
|
|
|
14
14
|
return discoverLocalBaseRef(root) || undefined;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
/** Opt-in local / multi-worktree cheap check. Ignored values: empty, 0, false, no, off. */
|
|
18
|
+
export function envFlagOn(value) {
|
|
19
|
+
return /^(1|true|yes)$/i.test(String(value ?? '').trim());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const LOCAL_STRICT_MERGE_MESSAGE =
|
|
23
|
+
'--local cannot be combined with --strict-merge: CI stays the full fail-closed check. Use --local (or ARK_CHECK_LOCAL=1) only on the local / pre-push path.';
|
|
24
|
+
|
|
25
|
+
export const LOCAL_TREE_MODE_MESSAGE =
|
|
26
|
+
'--local cannot be combined with report modes that need the whole tree (--doctor, --coverage, --plan, --report, --promote). Use --local --base <ref> for the cheap local check.';
|
|
27
|
+
|
|
28
|
+
export function localCheckEnvelope(args, root) {
|
|
29
|
+
if (!args?.local) return {};
|
|
30
|
+
return {
|
|
31
|
+
local: true,
|
|
32
|
+
scope: 'changed',
|
|
33
|
+
analysisRoot: path.resolve(root),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* `--local` / `ARK_CHECK_LOCAL=1` reuse `--changed`. Env is ignored under
|
|
39
|
+
* `--strict-merge` and under full-tree report modes so CI / doctor stay whole-tree.
|
|
40
|
+
*/
|
|
41
|
+
export function applyLocalCheckMode(args, env = process.env) {
|
|
42
|
+
const explicit = args.local === true;
|
|
43
|
+
const fromEnv = envFlagOn(env.ARK_CHECK_LOCAL);
|
|
44
|
+
if (!explicit && !fromEnv) return args;
|
|
45
|
+
const treeModes = [
|
|
46
|
+
args.doctor && '--doctor',
|
|
47
|
+
args.coverage && '--coverage',
|
|
48
|
+
args.plan && '--plan',
|
|
49
|
+
args.report && '--report',
|
|
50
|
+
args.promote && '--promote',
|
|
51
|
+
].filter(Boolean);
|
|
52
|
+
if (args.strictMerge) {
|
|
53
|
+
if (explicit) throw new Error(LOCAL_STRICT_MERGE_MESSAGE);
|
|
54
|
+
return args;
|
|
55
|
+
}
|
|
56
|
+
if (treeModes.length > 0) {
|
|
57
|
+
if (explicit) throw new Error(LOCAL_TREE_MODE_MESSAGE);
|
|
58
|
+
return args;
|
|
59
|
+
}
|
|
60
|
+
args.local = true;
|
|
61
|
+
args.changed = true;
|
|
62
|
+
return args;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseArgs(argv, env = process.env) {
|
|
18
66
|
const args = {
|
|
19
67
|
root: process.cwd(),
|
|
20
68
|
config: 'ark.config.json',
|
|
@@ -39,6 +87,7 @@ export function parseArgs(argv) {
|
|
|
39
87
|
contractSession: false,
|
|
40
88
|
contractDiff: false,
|
|
41
89
|
changed: false,
|
|
90
|
+
local: false,
|
|
42
91
|
against: undefined,
|
|
43
92
|
base: undefined,
|
|
44
93
|
persona: undefined,
|
|
@@ -180,6 +229,7 @@ export function parseArgs(argv) {
|
|
|
180
229
|
else if (arg === '--contract-session') args.contractSession = true;
|
|
181
230
|
else if (arg === '--contract-diff') args.contractDiff = true;
|
|
182
231
|
else if (arg === '--changed') args.changed = true;
|
|
232
|
+
else if (arg === '--local') args.local = true;
|
|
183
233
|
else if (arg === '--against') args.against = requireValue(arg, i++);
|
|
184
234
|
else if (arg === '--base') args.base = requireValue(arg, i++);
|
|
185
235
|
else if (arg === '--persona') args.persona = requireValue(arg, i++);
|
|
@@ -238,5 +288,5 @@ export function parseArgs(argv) {
|
|
|
238
288
|
);
|
|
239
289
|
}
|
|
240
290
|
}
|
|
241
|
-
return args;
|
|
291
|
+
return applyLocalCheckMode(args, env);
|
|
242
292
|
}
|
|
@@ -338,6 +338,8 @@ files; never weaken the gate via subagents.
|
|
|
338
338
|
|
|
339
339
|
**Escapes, not a second curriculum.** Do **not** run overlapping skills for the same job.
|
|
340
340
|
Pick **one** primary skill. Prefer doctor top action #1 when unsure.
|
|
341
|
+
Filter: **Contener · Guiar · Ordenar** — contain the write, guide the next step, order leftover mess. Skills never enforce. First-class extras:
|
|
342
|
+
\`/ark-runtime\` (ArkRun) · \`/ark-order\` (ArkOrder).
|
|
341
343
|
|
|
342
344
|
| When | Invoke | Not this |
|
|
343
345
|
|------|--------|----------|
|
|
@@ -352,10 +354,11 @@ Pick **one** primary skill. Prefer doctor top action #1 when unsure.
|
|
|
352
354
|
| Gate violation on a change (small cluster) | \`/ark-autopilot\` | leftover \`/ark-fix\` |
|
|
353
355
|
| Drive plan **A** to goal.met | \`/ark-autopilot\` | leftover \`/ark-loop\` |
|
|
354
356
|
| Ark **fitness** only (governed%, gates, baseline, install gaps) | \`/ark-coverage\` | leftover design work (use single path above) |
|
|
355
|
-
| One design decision, 2–3 options | \`/ark-
|
|
357
|
+
| One design decision, 2–3 options | \`/ark-explore\` (one decision) | leftover \`/ark-think\`; apply → autopilot |
|
|
356
358
|
| Explain / HTML report tour | \`/ark-explain\` | explore |
|
|
357
359
|
| Bump arkgate + refresh hosts | \`/ark-upgrade\` | — |
|
|
358
|
-
| Optional runtime kernel evaluate | \`/ark-runtime\` |
|
|
360
|
+
| Optional runtime kernel evaluate | \`/ark-runtime\` | leftover \`/ark-run\` (not a skill); order plane → \`/ark-order\` |
|
|
361
|
+
| Optional ArkOrder evaluate / wire | \`/ark-order\` | extra off → \`/ark-adopt\`; skip grind → \`/ark-autopilot\` |
|
|
359
362
|
|
|
360
363
|
**Post-green door:** when doctor reports ENFORCE · leftover design work, the **primary** next action is the single Shape path above — not a choice among explore / coverage / think. Doctor JSON: \`postGreenPath\` / \`primaryNextAction\`.
|
|
361
364
|
|
|
@@ -405,6 +408,8 @@ ${projectionBlock}
|
|
|
405
408
|
|
|
406
409
|
**Primary path (do this):**
|
|
407
410
|
|
|
411
|
+
Contener · Guiar · Ordenar — contain the write, guide the next step, order leftover mess.
|
|
412
|
+
|
|
408
413
|
1. Status anytime: \`${doctorCmd}\` — what's wrong and what to do first.
|
|
409
414
|
2. Read the **Improvement compass** section (not a score). Name residual lenses in plain language when present (SoC, DIP, domain, …). Out-of-scope lenses (performance, app security tooling, full resilience) stay honest — do not invent Ark enforcement for them.
|
|
410
415
|
3. CLI-first: if the local CLI already resolved this project root, do not wait on MCP “still connecting”. Identity handshake is optional in that case. When you do trust MCP evidence: call \`ark_identity\` with \`project.expectedRoot\` set to this project's exact absolute root, then reuse that root plus the returned \`projectIdentity.projectId\` on every Ark MCP call. A descendant path is authoritative only with that matching id. Missing tool, non-\`matched\` binding, or wrong root means this is not proven to be the right project: restart the host and use the local CLI meanwhile.
|
|
@@ -9,9 +9,40 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { ARK_ORDER_SCHEMA_DEF, ARK_RUN_SCHEMA_DEF, defaultedArkOrder, defaultedArkRun, validateArkOrderExtra, validateArkRunExtra, } from './config-extras.mjs';
|
|
12
|
+
import { canonicalStewardId } from './team-parliament.mjs';
|
|
12
13
|
/** Current published ark.config.json schema version (ADR 0027: 1.3 adds optional arkOrder). */
|
|
13
14
|
export const ARK_CONFIG_SCHEMA_VERSION = '1.3';
|
|
14
|
-
|
|
15
|
+
/** Closed layer trust tags. Optional; absence is silent. Not a schemaVersion bump. */
|
|
16
|
+
export const LAYER_TRUST_BOUNDARIES = ['public', 'auth', 'admin', 'internal'];
|
|
17
|
+
/** Future house: empty globs are expected; missing owners stay silent even when required. */
|
|
18
|
+
export function isFutureHouseLayer(layer) {
|
|
19
|
+
return layer?.optional === true || layer?.reserved === true || layer?.allowEmpty === true;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Present `layers[].owners` as non-empty strings. Absence / empty / wrong type → undefined.
|
|
23
|
+
*/
|
|
24
|
+
export function layerOwnersList(layer) {
|
|
25
|
+
const raw = layer && typeof layer === 'object' ? layer.owners : undefined;
|
|
26
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
27
|
+
return undefined;
|
|
28
|
+
const ids = raw.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
29
|
+
return ids.length > 0 ? ids : undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Layer names that must name owners when `requireLayerOwners` is on.
|
|
33
|
+
* Empty when the require flag is absent or false.
|
|
34
|
+
*/
|
|
35
|
+
export function layersMissingRequiredOwners(config) {
|
|
36
|
+
if (config?.requireLayerOwners !== true || !Array.isArray(config.layers))
|
|
37
|
+
return [];
|
|
38
|
+
return config.layers
|
|
39
|
+
.filter((layer) => !isFutureHouseLayer(layer) && !layerOwnersList(layer))
|
|
40
|
+
.map((layer) => layer.name);
|
|
41
|
+
}
|
|
42
|
+
export function missingLayerOwnersNextAction(layerName) {
|
|
43
|
+
return `Add a GitHub handle or email to ${layerName}'s owners in ark.config.json (/ark-adopt).`;
|
|
44
|
+
}
|
|
45
|
+
export const ARK_CONFIG_SCHEMA_URL = 'https://unpkg.com/arkgate@4/schemas/ark.config.schema.json';
|
|
15
46
|
const DEFAULT_LAYER_NAMES = [
|
|
16
47
|
'DomainModel',
|
|
17
48
|
'ApplicationOrchestration',
|
|
@@ -121,6 +152,11 @@ export const ARK_CONFIG_SCHEMA = {
|
|
|
121
152
|
arkOrder: { $ref: '#/$defs/arkOrder' },
|
|
122
153
|
/** Team parliament — GitHub handles or emails who may loosen the law (not part of policy hash). */
|
|
123
154
|
stewards: { ...stringArraySchema, default: [] },
|
|
155
|
+
/**
|
|
156
|
+
* When true, every non-reserved layer must name owners. Absence/false is
|
|
157
|
+
* silent. Policy teeth — stays in policyHash. Owners stay metadata.
|
|
158
|
+
*/
|
|
159
|
+
requireLayerOwners: { type: 'boolean' },
|
|
124
160
|
},
|
|
125
161
|
$defs: {
|
|
126
162
|
layer: {
|
|
@@ -133,6 +169,8 @@ export const ARK_CONFIG_SCHEMA = {
|
|
|
133
169
|
exclude: stringArraySchema,
|
|
134
170
|
intentPrefixes: stringArraySchema,
|
|
135
171
|
description: { type: 'string', minLength: 1 },
|
|
172
|
+
trustBoundary: { type: 'string', enum: [...LAYER_TRUST_BOUNDARIES] },
|
|
173
|
+
owners: { ...stringArraySchema, minItems: 1 },
|
|
136
174
|
forbiddenGlobals: stringArraySchema,
|
|
137
175
|
capabilities: {
|
|
138
176
|
type: 'object',
|
|
@@ -351,6 +389,30 @@ function validateNode(value, schema, path, root, issues) {
|
|
|
351
389
|
}
|
|
352
390
|
}
|
|
353
391
|
}
|
|
392
|
+
function validateLayerOwners(candidate, issues) {
|
|
393
|
+
const layers = candidate.layers;
|
|
394
|
+
if (!Array.isArray(layers))
|
|
395
|
+
return;
|
|
396
|
+
layers.forEach((layer, index) => {
|
|
397
|
+
if (!layer || typeof layer !== 'object' || Array.isArray(layer))
|
|
398
|
+
return;
|
|
399
|
+
if (!('owners' in layer))
|
|
400
|
+
return;
|
|
401
|
+
const owners = layer.owners;
|
|
402
|
+
if (!Array.isArray(owners))
|
|
403
|
+
return;
|
|
404
|
+
owners.forEach((entry, ownerIndex) => {
|
|
405
|
+
if (typeof entry !== 'string')
|
|
406
|
+
return;
|
|
407
|
+
if (!canonicalStewardId(entry)) {
|
|
408
|
+
issues.push({
|
|
409
|
+
path: `$.layers[${index}].owners[${ownerIndex}]`,
|
|
410
|
+
message: 'must be a GitHub handle or email (not a display name)',
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
}
|
|
354
416
|
function defaultedConfig(input) {
|
|
355
417
|
const result = {
|
|
356
418
|
...input,
|
|
@@ -456,6 +518,7 @@ export function loadArkConfigContract(input, source = 'ark.config.json') {
|
|
|
456
518
|
validateNode(candidate, ARK_CONFIG_SCHEMA, '$', ARK_CONFIG_SCHEMA, issues);
|
|
457
519
|
validateArkRunExtra(candidate, issues);
|
|
458
520
|
validateArkOrderExtra(candidate, issues);
|
|
521
|
+
validateLayerOwners(candidate, issues);
|
|
459
522
|
if (issues.length > 0)
|
|
460
523
|
throw new ArkConfigValidationError(source, issues);
|
|
461
524
|
return { config: candidate, migratedFrom };
|
|
@@ -493,6 +493,25 @@ export function summarizeDesignFitness(smells, ctx = {}) {
|
|
|
493
493
|
/** How many smell ids the green-run pointer names before it counts the rest. */
|
|
494
494
|
export const GREEN_PLAN_POINTER_MAX_IDS = 4;
|
|
495
495
|
|
|
496
|
+
/** When a smell has no evidence path, say so — do not leave the reader hunting. */
|
|
497
|
+
export const SMELL_UNATTRIBUTED =
|
|
498
|
+
'no file — leftover design, not a specific line';
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* One named smell with its first evidence path (or an explicit "no file").
|
|
502
|
+
* The pointer used to print only the id, so "1 design smell (domain-logic-in-ui)"
|
|
503
|
+
* could not answer "did my change cause this?"
|
|
504
|
+
*/
|
|
505
|
+
export function formatSmellPointerName(id, smells) {
|
|
506
|
+
const match = (smells || []).find((smell) => smell?.id === id);
|
|
507
|
+
const evidence = Array.isArray(match?.evidence)
|
|
508
|
+
? match.evidence.filter((entry) => typeof entry === 'string' && entry.trim())
|
|
509
|
+
: [];
|
|
510
|
+
if (evidence.length === 0) return `${id} (${SMELL_UNATTRIBUTED})`;
|
|
511
|
+
const extra = evidence.length - 1;
|
|
512
|
+
return extra > 0 ? `${id} at ${evidence[0]} (+${extra} more)` : `${id} at ${evidence[0]}`;
|
|
513
|
+
}
|
|
514
|
+
|
|
496
515
|
/**
|
|
497
516
|
* Name `--plan` on a run that passed.
|
|
498
517
|
*
|
|
@@ -528,7 +547,8 @@ export function formatGreenPlanPointer(smells, ctx = {}, planCommand = 'ark-chec
|
|
|
528
547
|
if (ids.length === 0) return null;
|
|
529
548
|
const shown = ids.slice(0, GREEN_PLAN_POINTER_MAX_IDS);
|
|
530
549
|
const hidden = ids.length - shown.length;
|
|
531
|
-
const
|
|
550
|
+
const namedIds = shown.map((id) => formatSmellPointerName(id, smells));
|
|
551
|
+
const named = hidden > 0 ? `${namedIds.join(', ')}, +${hidden} more` : namedIds.join(', ');
|
|
532
552
|
const plural = ids.length === 1 ? '' : 's';
|
|
533
553
|
const suppressed = ctx.suppressedCount ?? 0;
|
|
534
554
|
const opening =
|
|
@@ -32,7 +32,7 @@ function entry(ruleId, category, title, why, fix, extras) {
|
|
|
32
32
|
*/
|
|
33
33
|
export const DIAGNOSTIC_CATALOG = Object.freeze([
|
|
34
34
|
// ── layer / graph ────────────────────────────────────────────────────────
|
|
35
|
-
entry('LAYER_IMPORT_VIOLATION', 'layer', '
|
|
35
|
+
entry('LAYER_IMPORT_VIOLATION', 'layer', 'This import is not allowed', 'This file imported a folder it may not reach. The write doesn’t land. The same check fails the pull request.', 'Branch by import kind: constants/types/pure → adopt into DomainModel or SharedKernel (do not invent a port); kernel/events/bootstrap from Persistence → inject a port or move the map to SharedTypes (Persistence must not emit); define a port only when the target is a real use-case. Type-only edges use `import type`. Then preflight again. Do not weaken the layer rule without a hash-bound policy acknowledgement.'),
|
|
36
36
|
entry('LAYER_INTENT_REFERENCE_VIOLATION', 'layer', 'Intent referenced across a blocked layer edge', 'A string intent (or intent-like reference) names a layer that the file’s layer may not reach under the contract rules — the same plane as import edges, for event/intent coupling.', 'Reference that intent from a layer allowed to know about it (usually an adapter or application layer), or relocate the reference — then preflight again.'),
|
|
37
37
|
entry('LAYER_REFERENCE_VIOLATION', 'layer', 'Layer reference blocked (snippet / AI gate)', 'Snippet analysis found an intent or string reference that would couple layers in a direction the architecture profile forbids.', 'Move the reference to an allowed layer or introduce a port/event boundary, then re-run the snippet gate.'),
|
|
38
38
|
entry('CIRCULAR_DEPENDENCY', 'layer', 'Dependency cycle', 'Two or more modules import each other in a loop. Cycles make ownership unclear and break stable layer direction.', 'Extract the shared dependency into a third module, invert one edge behind a port, or merge units that are truly one — then preflight again.'),
|
|
@@ -56,6 +56,7 @@ export const DIAGNOSTIC_CATALOG = Object.freeze([
|
|
|
56
56
|
entry('ARKRULE_INVARIANT', 'arkrules', 'ArkRule invariant failed', 'Reserved / remediation-recognized code for invariant-plane failures bound to an ArkRule id (coverage path also emits INVARIANT_UNCOVERED).', 'Fix the invariant for the ArkRule declared in arkrules/<Layer>.json, then preflight again. Do not demote without acknowledgement.'),
|
|
57
57
|
entry('ARKRULE_SCOPE_EMPTY', 'arkrules', 'ArkRule appliesTo matched zero files', 'An ArkRule’s appliesTo globs matched no governed files — the rule cannot observe what it claims to protect.', 'Fix appliesTo globs so they match governed files, or remove the rule. Enforced empty scope fails; advisory empty scope warns.', { oftenAdvisory: true }),
|
|
58
58
|
entry('ARKRULE_HINT_BUDGET_EXHAUSTED', 'arkrules', 'Structural-hint budget exhausted', 'orchestration-only, thin-adapter, and writes-via-aggregate only evaluate files the hint loader preloaded. When eligible governed files exceed that budget (coverage.maxFiles, default 400 — there is no arkrules.hintBudget), those sensors never saw the rest of their scope. Enforced + unreviewed is not green. The finding names exact hinted/governed counts and per-sensor reviewed N/M of scope.', 'Raise coverage.maxFiles in ark.config.json (this cap also bounds structural-hint preload; --doctor names the coupling) so hinted/governed counts match, then re-run with --strict-config. An enforced hint sensor that cannot see its scope fails strict.'),
|
|
59
|
+
entry('INVARIANT_CATALOG_EMPTY', 'arkrules', 'Domain invariant catalog is empty', 'ArkRules is on and a Domain-role layer has code, but invariants[] has no phrases the code must preserve. Empty looks like “done” until someone fills the catalog.', 'Add 1–2 short phrases to invariants[] in arkrules/<Domain>.json (or the mapped file). Starters show the shape. Advisory unless a domain structure rule is already enforced — then --strict-merge can refuse. Do not freeze this finding.', { oftenAdvisory: true }),
|
|
59
60
|
entry('INVARIANT_UNCOVERED', 'arkrules', 'Invariant without coverage evidence', 'An ArkRules invariant is under contract but no covering test title or declared symbol evidence was found (or coverage is partial). Kind is never-had-tests (adopt residual) vs tests-disappeared (suite exists).', 'Add a test title or declared symbol covering the arkruleId, then preflight again. Treat never-had-tests as adopt residual; treat tests-disappeared as a regression. Missing test globs report partial — never fake green. When the message reports an exhausted file budget, raise coverage.maxFiles (or narrow coverage.testGlobs) in ark.config.json.'),
|
|
60
61
|
entry('INVARIANT_COVERAGE_OUTSIDE_ROOTS', 'arkrules', 'Covering test outside the declared coverage roots', 'The only test naming this invariant sits outside coverage.coverageRoots — the places the project declares its runner executes. ArkGate matches declared text and never executes tests, so it cannot tell whether that file is ever run: coverage there is a test that exists, not a test that runs.', 'Move the test under a declared coverage root, or add its root to coverage.coverageRoots in ark.config.json. Advisory: it never fails strict, but promotion to enforced refuses on it.', { oftenAdvisory: true }),
|
|
61
62
|
// ── ArkRun (opt-in extra; RN05 dual-depth nextAction) ────────────────────
|
|
@@ -75,7 +76,7 @@ export const DIAGNOSTIC_CATALOG = Object.freeze([
|
|
|
75
76
|
entry('ARKORDER_INFORMATION_BUDGET', 'arkorder', 'Projection observes a forbidden kind', 'h(ξ) allowedKinds includes a kind listed in informationBudget.cannotObserve. A scale may not look at what it was told not to see.', 'Cut that kind from the projector or from cannotObserve, then preflight again. Never mechanical-safe.'),
|
|
76
77
|
entry('ARKORDER_XI_TTL', 'arkorder', 'Slow key carries a freshness field', 'ξ named ttl/freshUntil/maxAge. Freshness belongs on σ. A slow parameter that expires per transaction is not slow.', 'Move freshness onto σ (freshUntil) and keep ξ stable, then preflight again. Never mechanical-safe.'),
|
|
77
78
|
entry('ARKORDER_STALE_SIGMA', 'arkorder', 'σ is stale', 'ingest ran after σ.freshUntil (or sigmaMaxAgeMs). ξ does not TTL.', 'Call refreshSigma and ingest again, or proposeRelease then apply(ProposeResult) if the pattern changed. Never mechanical-safe.'),
|
|
78
|
-
entry('ARKORDER_UNVALVED_RELEASE', 'arkorder', '
|
|
79
|
+
entry('ARKORDER_UNVALVED_RELEASE', 'arkorder', 'Second freeze without the valve', 'release() already froze the big choice. A later release() with a different value does not land. First freeze is release(); later change is proposeRelease then apply.', 'Change the choice with proposeRelease then apply. release() is only the first freeze. Never mechanical-safe.'),
|
|
79
80
|
// ── atomic preflight / change set ────────────────────────────────────────
|
|
80
81
|
entry('INVALID_CHANGE_PATH', 'preflight', 'Unsafe change path', 'A change set entry is not a safe, non-empty project-relative path (absolute, escape, empty, or NUL).', 'Use canonical project-relative paths only in the atomic change set, then preflight again.'),
|
|
81
82
|
entry('DUPLICATE_CHANGE_PATH', 'preflight', 'Duplicate path in change set', 'The atomic change set lists more than one operation for the same path.', 'Collapse to one create/update/delete per path, then preflight again.'),
|
|
@@ -90,7 +91,7 @@ export const DIAGNOSTIC_CATALOG = Object.freeze([
|
|
|
90
91
|
entry('DESIGN_SMELL_REGRESSION', 'preflight', 'Design smell regression on base-relative ratchet', 'Compared to the base ref, the candidate introduces a created-path domain-logic-in-ui file under --strict-merge, or introduces or worsens a blocking design-smell class under --fail-on-new-smells.', 'Move the new UI business rule out of the created file (or revert a --fail-on-new-smells regression), then re-run with the same base ref.'),
|
|
91
92
|
// ── analysis completeness / host ─────────────────────────────────────────
|
|
92
93
|
entry('ANALYSIS_PARSE_INCOMPLETE', 'analysis', 'Parse incomplete', 'Governed source could not be fully parsed; evidence includes the TypeScript diagnostic (line + message). Incremental mid-edit parse is normal for agents. Contract exclude paths skip the write hook.', 'Finish the source or fix the reported syntax error, then re-run `npx arkgate-check`. The write hook does not deny solely on mid-edit parse. Partial never means pass.'),
|
|
93
|
-
entry('LEXICAL_EVIDENCE_INCOMPLETE', 'analysis', 'Lexical evidence incomplete', '
|
|
94
|
+
entry('LEXICAL_EVIDENCE_INCOMPLETE', 'analysis', 'Lexical evidence incomplete', 'This check only saw one file, so it cannot fully prove how the import resolves. The result is provisional — `ark-check` on the project is the authority.', 'Run `npx arkgate-check --root . --config ark.config.json` to confirm. Do not call ark_prepare_change from a hook deny.'),
|
|
94
95
|
entry('ANALYSIS_COVERS_NO_FILES', 'analysis', 'Analysis covered no files', 'No file matched the contract include and layer patterns under the analyzed root, so the run had nothing to check. Every rule is vacuously satisfied on an empty set: a green here would read exactly like a green over a governed tree while certifying nothing. Usual causes are a --root that is not the tree the contract describes (including a contract found outside the requested root, whose directory is then adopted as the project root), include / exclude patterns that match nothing, or layer patterns written for a different layout.', 'Point --root at the tree the contract describes, or keep the contract inside that tree, or fix the include / exclude / layer patterns so they match real files — then re-run `npx arkgate-check --root . --config ark.config.json`. This is a refusal about ArkGate\u2019s own inputs, not a finding about your code; no baseline or policy acknowledgement can suppress it.'),
|
|
95
96
|
entry('ANALYSIS_HOST_UNAVAILABLE', 'analysis', 'Analysis host unavailable', 'No usable TypeScript / analysis host was available for this invocation.', 'Install a supported TypeScript version visible to the project, then re-run. Unavailable analysis is fail-closed.'),
|
|
96
97
|
entry('ADAPTER_NOT_ALLOWED_FOR_PORT', 'adapter', 'Adapter not allowed for port', 'Runtime/port wiring selected an adapter implementation that the architecture profile does not allow for that port.', 'Bind an allowed adapter for the port, or adjust the profile with an explicit policy decision — then re-run.'),
|
|
@@ -116,6 +117,7 @@ export const DIAGNOSTIC_CATALOG = Object.freeze([
|
|
|
116
117
|
entry('CONFIG_RULE_UNKNOWN_TO_LAYER', 'config', 'Rule unknown to layer', 'A dependency rule references a target layer name that is not declared.', 'Fix the rule’s to field to a declared layer name.', { oftenAdvisory: true }),
|
|
117
118
|
entry('CONFIG_AMBIGUOUS_LAYERS', 'config', 'Ambiguous layer classification', 'Some files match multiple layers at equal specificity; classification falls back to declaration order.', 'Disambiguate overlapping patterns so each file has one clear layer owner.', { oftenAdvisory: true }),
|
|
118
119
|
entry('CONFIG_UNCLASSIFIED_FILES', 'config', 'Unclassified included files', 'Included source files match no layer pattern; import rules will not enforce on them.', 'Extend layer patterns or narrow include so every governed file is classified.', { oftenAdvisory: true }),
|
|
120
|
+
entry('CONFIG_LAYER_MISSING_OWNER', 'config', 'Layer missing owner', 'requireLayerOwners is on and this layer has no owners. Writes to that house fail closed, like a folder with no name on the door.', 'Add a GitHub handle or email to that layer’s owners in ark.config.json (/ark-adopt), then re-run. Reserved/allowEmpty houses may stay unnamed. Turn the flag off only if you no longer want owners required.'),
|
|
119
121
|
// ── literal path drift ───────────────────────────────────────────────────
|
|
120
122
|
entry('LITERAL_PATH_DRIFT', 'drift', 'Literal path moved by a rename', 'A repo path written inside a string, a comment or a docstring no longer resolves, and the rename set says where it went. Nothing in the gate sees this class: `tsc` resolves imports, not strings, and ESLint does not either, so the rename compiles green and the reference lies afterwards. It appears in four forms — the tsconfig alias, a relative literal, a path written without the include-root prefix, and prose — and a hand sweep reliably covers one of them.', 'Apply the suggested replacement, or re-run `npx arkgate-check --path-drift --base-ref <ref> --write` to apply every writable anchored replacement at once. The rewrite is mechanical and one-directional: the destination comes from the rename, it must itself resolve and be path-shaped, and the token is rewritten in the form the author wrote it in. A destination that leaves the alias root of the literal is reported with the target only and must be rewritten by hand.'),
|
|
121
123
|
entry('LITERAL_PATH_UNRESOLVED', 'drift', 'Literal path does not resolve', 'A literal that looks like a repo path does not resolve under this root, and no rename explains where it went. Unlike LITERAL_PATH_DRIFT this is a candidate, not a verdict: with nothing to anchor it, ArkGate cannot tell a dead reference from an illustrative path in a comment, an example in documentation, or a path belonging to another tree.', 'Read the candidate and decide: fix the path, or leave it. Advisory only — it never fails a run and is never rewritten by --write, because there is no destination to propose. Run `--path-drift --all` to list the sweep.', { oftenAdvisory: true }),
|