arkgate 4.8.14 → 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 +133 -0
- package/README.md +13 -5
- package/bin/ark-check-runtime.mjs +17 -49
- 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-skills-package.mjs +179 -1
- package/bin/lib/analysis-completeness.mjs +38 -2
- package/bin/lib/analysis-engine.mjs +6 -6
- package/bin/lib/architecture-scan.mjs +26 -4
- 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/config-contract.mjs +64 -1
- package/bin/lib/diagnostic-catalog.mjs +3 -1
- package/bin/lib/doctor-advisories.mjs +94 -10
- package/bin/lib/doctor-human.mjs +18 -5
- 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 +8 -1
- 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/layer-description.mjs +90 -8
- package/bin/lib/mcp-hook-payload.mjs +56 -0
- package/bin/lib/no-domain-frontend.mjs +91 -0
- package/bin/lib/presets.mjs +3 -4
- package/bin/lib/product-copy.mjs +15 -0
- package/bin/lib/projected-governed-coverage.mjs +114 -0
- package/bin/lib/recommend-cli.mjs +54 -0
- package/bin/lib/remediation.mjs +4 -0
- package/bin/lib/resolved-candidate-facts.mjs +83 -66
- package/bin/lib/rules-under-contract.mjs +117 -2
- package/bin/lib/snippet-analysis.mjs +14 -8
- package/bin/lib/start-preview.mjs +5 -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/violations.mjs +13 -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-DVx_2RmF.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 +70 -9
- 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 +1 -1
- package/docs/agent-guide.md +6 -4
- package/docs/ai-gates.md +10 -3
- package/docs/brownfield-adoption.md +4 -1
- package/docs/configuration.md +55 -7
- package/docs/develop.md +1 -0
- package/docs/diagnostics.md +23 -3
- package/docs/enthusiast/how-to-agent-gates.md +5 -0
- package/docs/enthusiast/how-to-pick-shape.md +1 -1
- package/docs/package-surface.md +7 -5
- package/docs/use.md +5 -0
- package/package.json +1 -1
- package/schemas/ark.config.schema.json +23 -2
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +82 -1
- package/templates/agent-skills/ark-autopilot/SKILL.md +6 -2
- package/templates/agent-skills/ark-coverage/SKILL.md +1 -1
- package/templates/agent-skills/ark-explain/SKILL.md +4 -2
- package/templates/agent-skills/ark-explore/SKILL.md +14 -1
- package/templates/agent-skills/ark-place/SKILL.md +38 -3
- package/templates/agent-skills/ark-runtime/SKILL.md +4 -2
- package/templates/agent-skills/ark-upgrade/SKILL.md +1 -1
- package/templates/arkrules/DomainModel.json +14 -1
- package/templates/skills/ark-adopt.md +82 -1
- package/templates/skills/ark-autopilot.md +6 -2
- package/templates/skills/ark-coverage.md +1 -1
- package/templates/skills/ark-explain.md +4 -2
- package/templates/skills/ark-explore.md +14 -1
- package/templates/skills/ark-place.md +38 -3
- package/templates/skills/ark-runtime.md +4 -2
- package/templates/skills/ark-upgrade.md +1 -1
|
@@ -308,6 +308,62 @@ export function emitHostAllow(output, { antigravityStyle, cursorStyle }) {
|
|
|
308
308
|
emitCursorAllow(output, cursorStyle);
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
/** Host-native deny envelopes. Exit 2 is still set by the caller. */
|
|
312
|
+
export function emitHostDeny(output, { antigravityStyle, cursorStyle, grokStyle, message, file }) {
|
|
313
|
+
const text = String(message || '').endsWith('\n') ? String(message) : `${message}\n`;
|
|
314
|
+
output.stderr(text);
|
|
315
|
+
if (antigravityStyle || grokStyle) {
|
|
316
|
+
output.stdout(`${JSON.stringify({ decision: 'deny', reason: String(message || '').trim() })}\n`);
|
|
317
|
+
}
|
|
318
|
+
if (cursorStyle) {
|
|
319
|
+
output.stdout(
|
|
320
|
+
JSON.stringify({
|
|
321
|
+
permission: 'deny',
|
|
322
|
+
agent_message: String(message || '').trim(),
|
|
323
|
+
user_message: `ArkGate blocked write to ${file || 'this file'}`,
|
|
324
|
+
}) + '\n'
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** File-local twin of CONFIG_UNCLASSIFIED_FILES — included, no layer, write must not land. */
|
|
330
|
+
export function unclassifiedIncludedWriteDeny(relativePath) {
|
|
331
|
+
const file = String(relativePath || 'this file');
|
|
332
|
+
return {
|
|
333
|
+
ruleId: 'CONFIG_UNCLASSIFIED_FILES',
|
|
334
|
+
message: `${file} is included but matches no layer, so import rules will not run on it.`,
|
|
335
|
+
nextAction:
|
|
336
|
+
'Put it in a layer folder with /ark-place, or extend layer patterns / narrow include.',
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** File-local twin of CONFIG_LAYER_MISSING_OWNER — required owners, this house has none. */
|
|
341
|
+
export function unownedLayerWriteDeny(relativePath, layerName) {
|
|
342
|
+
const file = String(relativePath || 'this file');
|
|
343
|
+
const house = String(layerName || 'this layer');
|
|
344
|
+
return {
|
|
345
|
+
ruleId: 'CONFIG_LAYER_MISSING_OWNER',
|
|
346
|
+
message: `${file} is in ${house}, and that folder has no owner.`,
|
|
347
|
+
nextAction: `Add a GitHub handle or email to ${house}'s owners in ark.config.json (/ark-adopt).`,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Fail-closed write when requireLayerOwners is on and this house has no owners.
|
|
353
|
+
* Silent when the flag is off, the layer is reserved, or owners are present.
|
|
354
|
+
*/
|
|
355
|
+
export function requiredOwnerWriteDeny(config, layerName, relativePath) {
|
|
356
|
+
if (config?.requireLayerOwners !== true || !layerName) return null;
|
|
357
|
+
const layer = (config.layers ?? []).find((entry) => entry?.name === layerName);
|
|
358
|
+
if (!layer) return null;
|
|
359
|
+
if (layer.optional === true || layer.reserved === true || layer.allowEmpty === true) return null;
|
|
360
|
+
const owners = Array.isArray(layer.owners)
|
|
361
|
+
? layer.owners.filter((entry) => typeof entry === 'string' && entry.length > 0)
|
|
362
|
+
: [];
|
|
363
|
+
if (owners.length > 0) return null;
|
|
364
|
+
return unownedLayerWriteDeny(relativePath, layerName);
|
|
365
|
+
}
|
|
366
|
+
|
|
311
367
|
/**
|
|
312
368
|
* Socket-style write-gate deny: two lines first. Pass/fail, no score.
|
|
313
369
|
* Rule id stays on a following line, not the first sentence.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Soft no-Domain / all-logic-in-frontend residual (P2 §3 / Guiar).
|
|
3
|
+
* Tooling I/O. Never a gate fail.
|
|
4
|
+
*
|
|
5
|
+
* Projects facts doctor already has: empty Domain-role layer + presentation
|
|
6
|
+
* file share, or the existing `domain-logic-in-ui` smell. No second tree walk.
|
|
7
|
+
* Silent when there is no frontend, Domain already has files, or the bag is
|
|
8
|
+
* too thin to call “all logic in the UI.”
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { isDomainRoleLayerName } from './arkrules-sensors.mjs';
|
|
12
|
+
|
|
13
|
+
export const NO_DOMAIN_FRONTEND_ASK =
|
|
14
|
+
'Business rules live in the UI, and Domain is still empty.';
|
|
15
|
+
|
|
16
|
+
export const NO_DOMAIN_FRONTEND_NEXT =
|
|
17
|
+
'Put one pure rule in a Domain file (/ark-place), then one small refactor with /ark-autopilot. Do not pile more rules in pages.';
|
|
18
|
+
|
|
19
|
+
/** Presentation-role house by name. Align with design-smells presentation heuristic. */
|
|
20
|
+
export function isPresentationRoleLayerName(name) {
|
|
21
|
+
return typeof name === 'string' && /presentation|ui|view|frontend/i.test(name);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function layerRows(input) {
|
|
25
|
+
return Array.isArray(input.coverage?.layers) ? input.coverage.layers : [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function configLayers(input) {
|
|
29
|
+
return Array.isArray(input.config?.layers) ? input.config.layers : [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function emptyLayerSet(input) {
|
|
33
|
+
return new Set(Array.isArray(input.coverage?.emptyLayers) ? input.coverage.emptyLayers : []);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function totalGovernedFiles(input) {
|
|
37
|
+
const governed = input.coverage?.governed?.totalFiles;
|
|
38
|
+
if (Number.isFinite(governed) && governed >= 0) return governed;
|
|
39
|
+
const total = input.coverage?.totalFiles;
|
|
40
|
+
return Number.isFinite(total) && total >= 0 ? total : 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function hasDomainLogicInUiSmell(designSmells) {
|
|
44
|
+
return (Array.isArray(designSmells) ? designSmells : []).some(
|
|
45
|
+
(smell) => smell && smell.id === 'domain-logic-in-ui'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Soft residual when Domain is declared but empty and the UI holds the rules.
|
|
51
|
+
* Never flips valid / goal.met.
|
|
52
|
+
*
|
|
53
|
+
* @param {{ config?: object, coverage?: object, designSmells?: object[] }} [input]
|
|
54
|
+
* @returns {{ kind: 'ui-logic' | 'presentation-bag', ask: string, nextAction: string, domainLayers: string[], presentationFiles: number } | null}
|
|
55
|
+
*/
|
|
56
|
+
export function collectNoDomainFrontendResidual(input = {}) {
|
|
57
|
+
const totalFiles = totalGovernedFiles(input);
|
|
58
|
+
if (totalFiles <= 0) return null;
|
|
59
|
+
|
|
60
|
+
const declared = configLayers(input);
|
|
61
|
+
const domainLayers = declared.filter((layer) =>
|
|
62
|
+
isDomainRoleLayerName(layer.name, layer.intentPrefixes ?? [])
|
|
63
|
+
);
|
|
64
|
+
if (domainLayers.length === 0) return null;
|
|
65
|
+
|
|
66
|
+
const rows = layerRows(input);
|
|
67
|
+
const empty = emptyLayerSet(input);
|
|
68
|
+
const domainEmpty = domainLayers.every((layer) => {
|
|
69
|
+
const row = rows.find((entry) => entry.name === layer.name);
|
|
70
|
+
const files = Number(row?.files) || 0;
|
|
71
|
+
return files === 0 || empty.has(layer.name);
|
|
72
|
+
});
|
|
73
|
+
if (!domainEmpty) return null;
|
|
74
|
+
|
|
75
|
+
const presentationFiles = rows
|
|
76
|
+
.filter((row) => isPresentationRoleLayerName(row.name))
|
|
77
|
+
.reduce((sum, row) => sum + (Number(row.files) || 0), 0);
|
|
78
|
+
if (presentationFiles <= 0) return null;
|
|
79
|
+
|
|
80
|
+
const uiLogic = hasDomainLogicInUiSmell(input.designSmells);
|
|
81
|
+
const presentationShare = presentationFiles / totalFiles;
|
|
82
|
+
if (!uiLogic && (presentationShare < 0.5 || presentationFiles < 3)) return null;
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
kind: uiLogic ? 'ui-logic' : 'presentation-bag',
|
|
86
|
+
ask: NO_DOMAIN_FRONTEND_ASK,
|
|
87
|
+
nextAction: NO_DOMAIN_FRONTEND_NEXT,
|
|
88
|
+
domainLayers: domainLayers.map((layer) => layer.name),
|
|
89
|
+
presentationFiles,
|
|
90
|
+
};
|
|
91
|
+
}
|
package/bin/lib/presets.mjs
CHANGED
|
@@ -670,8 +670,7 @@ export const ARCHITECTURE_PRESETS = {
|
|
|
670
670
|
},
|
|
671
671
|
{
|
|
672
672
|
name: 'ApplicationOrchestration',
|
|
673
|
-
description:
|
|
674
|
-
'Use cases and services that coordinate the domain through ports. Next App Router API (`app/api/**`) and Pages API (`pages/api/**`) are orchestration shells, not UI.',
|
|
673
|
+
description: 'Use cases and services that coordinate the domain through ports.',
|
|
675
674
|
patterns: [
|
|
676
675
|
'**/application/**',
|
|
677
676
|
'**/use-cases/**',
|
|
@@ -687,7 +686,7 @@ export const ARCHITECTURE_PRESETS = {
|
|
|
687
686
|
{
|
|
688
687
|
name: 'PresentationAdapters',
|
|
689
688
|
description:
|
|
690
|
-
'Entrypoints — UI,
|
|
689
|
+
'Entrypoints — UI, pages, controllers when present. Never bare lib/** (data clients are Persistence).',
|
|
691
690
|
patterns: [
|
|
692
691
|
'**/app/**',
|
|
693
692
|
'**/pages/**',
|
|
@@ -760,7 +759,7 @@ export const ARCHITECTURE_PRESETS = {
|
|
|
760
759
|
{
|
|
761
760
|
name: 'ApplicationOrchestration',
|
|
762
761
|
description:
|
|
763
|
-
'Server actions, features,
|
|
762
|
+
'Server actions, features, and non-UI lib orchestration (when present).',
|
|
764
763
|
patterns: [
|
|
765
764
|
'src/features/**',
|
|
766
765
|
'src/server/**',
|
package/bin/lib/product-copy.mjs
CHANGED
|
@@ -10,6 +10,21 @@
|
|
|
10
10
|
export const NORTH_STAR_ONE_LINE =
|
|
11
11
|
'Contener · Guiar · Ordenar — contain the write, guide the next step, order leftover mess.';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Compact doctor / details — only when `arkRules` is on.
|
|
15
|
+
* Absence stays silent (unlike ArkOrder, which speaks when off).
|
|
16
|
+
*/
|
|
17
|
+
export const ARKRULES_ONE_BREATH =
|
|
18
|
+
'Layers stop a bad import. ArkRules is optional policies inside one folder.';
|
|
19
|
+
|
|
20
|
+
/** Next step when the map is on and something still needs a human. */
|
|
21
|
+
export const ARKRULES_FIRST_CONTACT_NEXT =
|
|
22
|
+
'Next: ark-check --rules-inventory — then /ark-adopt to write one rule, or /ark-autopilot to promote.';
|
|
23
|
+
|
|
24
|
+
/** Next step when Domain has code but invariants[] is empty. */
|
|
25
|
+
export const ARKRULES_EMPTY_CATALOG_NEXT =
|
|
26
|
+
'Next: add 1–2 short phrases to invariants[] in the Domain rules file, then ark-check --doctor.';
|
|
27
|
+
|
|
13
28
|
/** Status-light leftover-design qualifier (was “design-weak”). */
|
|
14
29
|
export const LEFTOVER_DESIGN_LABEL = 'leftover design work';
|
|
15
30
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One definition of projected governed coverage: include-scoped files that
|
|
3
|
+
* match a layer glob, via computeCoverage. Shared by start, --recommend, and
|
|
4
|
+
* doctor so the same tree cannot advertise two "projected" percents.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { collectGovernedFiles } from './scan-files.mjs';
|
|
9
|
+
import { computeCoverage } from './doctor-plan.mjs';
|
|
10
|
+
import { ARCHITECTURE_PRESETS } from './presets.mjs';
|
|
11
|
+
import {
|
|
12
|
+
detectWorkspaces,
|
|
13
|
+
resolveIncludeRoots,
|
|
14
|
+
resolveStartInitPreset,
|
|
15
|
+
} from '../ark-shared.mjs';
|
|
16
|
+
|
|
17
|
+
export function measureGovernedCoverage(root, config) {
|
|
18
|
+
const files = collectGovernedFiles(root, config);
|
|
19
|
+
return computeCoverage(root, config, files, config.rules ?? []);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function summarizeGovernedCoverage(coverage) {
|
|
23
|
+
const governed = coverage?.governed ?? {};
|
|
24
|
+
return {
|
|
25
|
+
percent: typeof governed.percent === 'number' ? governed.percent : 0,
|
|
26
|
+
classifiedFiles: governed.classifiedFiles ?? 0,
|
|
27
|
+
totalFiles: governed.totalFiles ?? coverage?.totalFiles ?? 0,
|
|
28
|
+
emptyScope: coverage?.emptyScope === true,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function starterConfigForPreset(root, preset) {
|
|
33
|
+
const factory = ARCHITECTURE_PRESETS[preset];
|
|
34
|
+
if (typeof factory !== 'function') return null;
|
|
35
|
+
const workspacesOrInclude =
|
|
36
|
+
preset === 'monorepo' || preset === 'ui-surface'
|
|
37
|
+
? resolveIncludeRoots(root)
|
|
38
|
+
: detectWorkspaces(root);
|
|
39
|
+
return factory(workspacesOrInclude, root);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Contract start/recommend/doctor should score: existing ark.config.json, else
|
|
44
|
+
* the starter --init would write for this tree (same preset resolution as start).
|
|
45
|
+
*/
|
|
46
|
+
export function resolveProjectedCoverageConfig(root, recommendation = {}) {
|
|
47
|
+
const configPath = path.join(root, 'ark.config.json');
|
|
48
|
+
if (fs.existsSync(configPath)) {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
51
|
+
if (parsed && typeof parsed === 'object') {
|
|
52
|
+
return { config: parsed, source: 'existing-config' };
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
/* fall through to starter */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const preset = resolveStartInitPreset(root, recommendation);
|
|
59
|
+
if (!preset) return { config: null, source: 'none', preset: null };
|
|
60
|
+
return { config: starterConfigForPreset(root, preset), source: 'start-starter', preset };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function measureProjectedGovernedCoverage(root, recommendation = {}) {
|
|
64
|
+
const resolved = resolveProjectedCoverageConfig(root, recommendation);
|
|
65
|
+
if (!resolved.config) {
|
|
66
|
+
return {
|
|
67
|
+
percent: 0,
|
|
68
|
+
classifiedFiles: 0,
|
|
69
|
+
totalFiles: 0,
|
|
70
|
+
emptyScope: true,
|
|
71
|
+
source: resolved.source,
|
|
72
|
+
preset: resolved.preset ?? null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
...summarizeGovernedCoverage(measureGovernedCoverage(root, resolved.config)),
|
|
77
|
+
source: resolved.source,
|
|
78
|
+
preset: resolved.preset ?? recommendation.preset ?? null,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const PROJECTED_COVERAGE_REASON = /projected governed coverage is \d+% \(below 90%\)/;
|
|
83
|
+
|
|
84
|
+
export function withProjectedGovernedCoverage(recommendation, root) {
|
|
85
|
+
if (!recommendation || typeof recommendation !== 'object') return recommendation;
|
|
86
|
+
const measured = measureProjectedGovernedCoverage(root, recommendation);
|
|
87
|
+
const percent = measured.percent;
|
|
88
|
+
const confirmationReasons = (recommendation.confirmationReasons ?? []).filter(
|
|
89
|
+
(reason) => !PROJECTED_COVERAGE_REASON.test(String(reason))
|
|
90
|
+
);
|
|
91
|
+
if (percent < 90) {
|
|
92
|
+
confirmationReasons.push(`projected governed coverage is ${percent}% (below 90%)`);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
...recommendation,
|
|
96
|
+
requiresConfirmation:
|
|
97
|
+
Boolean(recommendation.requiresConfirmation) ||
|
|
98
|
+
percent < 90 ||
|
|
99
|
+
Boolean(recommendation.thinTsSurface),
|
|
100
|
+
confirmationReasons,
|
|
101
|
+
signals: {
|
|
102
|
+
...recommendation.signals,
|
|
103
|
+
projectedGovernedCoverage: percent,
|
|
104
|
+
projectedGovernedClassifiedFiles: measured.classifiedFiles,
|
|
105
|
+
projectedGovernedTotalFiles: measured.totalFiles,
|
|
106
|
+
projectedGovernedSource: measured.source,
|
|
107
|
+
},
|
|
108
|
+
projectedCoverage: {
|
|
109
|
+
percent,
|
|
110
|
+
classifiedFiles: measured.classifiedFiles,
|
|
111
|
+
totalFiles: measured.totalFiles,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--recommend` presentation. Lives here so ark-check-runtime stays under the
|
|
3
|
+
* orchestration LOC budget and doctor does not parse this path.
|
|
4
|
+
*/
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
ADOPTION_PLAN_FILENAME,
|
|
8
|
+
buildArchitectureRecommendation,
|
|
9
|
+
formatArchitectureRecommendationHuman,
|
|
10
|
+
writeAdoptionPlan,
|
|
11
|
+
} from '../ark-shared.mjs';
|
|
12
|
+
import { withProjectedGovernedCoverage } from './projected-governed-coverage.mjs';
|
|
13
|
+
|
|
14
|
+
export function runRecommend(args) {
|
|
15
|
+
try {
|
|
16
|
+
const recommendation = withProjectedGovernedCoverage(
|
|
17
|
+
buildArchitectureRecommendation(args.root),
|
|
18
|
+
args.root
|
|
19
|
+
);
|
|
20
|
+
let planWritten;
|
|
21
|
+
if (args.writePlan) {
|
|
22
|
+
const result = writeAdoptionPlan(args.root, recommendation);
|
|
23
|
+
planWritten = result.path;
|
|
24
|
+
}
|
|
25
|
+
if (args.json) {
|
|
26
|
+
console.log(
|
|
27
|
+
JSON.stringify(
|
|
28
|
+
{
|
|
29
|
+
...recommendation,
|
|
30
|
+
...(planWritten
|
|
31
|
+
? { adoptionPlanPath: path.relative(args.root, planWritten) || ADOPTION_PLAN_FILENAME }
|
|
32
|
+
: {}),
|
|
33
|
+
},
|
|
34
|
+
null,
|
|
35
|
+
2
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
} else {
|
|
39
|
+
console.log(formatArchitectureRecommendationHuman(recommendation));
|
|
40
|
+
if (planWritten) {
|
|
41
|
+
console.log('');
|
|
42
|
+
console.log(`Wrote ${path.relative(args.root, planWritten) || ADOPTION_PLAN_FILENAME}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
47
|
+
if (args.json) {
|
|
48
|
+
console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
49
|
+
} else {
|
|
50
|
+
console.error(`ark-check --recommend failed: ${message}`);
|
|
51
|
+
}
|
|
52
|
+
process.exitCode = 2;
|
|
53
|
+
}
|
|
54
|
+
}
|
package/bin/lib/remediation.mjs
CHANGED
|
@@ -216,6 +216,10 @@ export function deterministicNextAction(violation) {
|
|
|
216
216
|
return 'Add metadata.source to the publish call, then run Ark again.';
|
|
217
217
|
case 'INVARIANT_COVERAGE_OUTSIDE_ROOTS':
|
|
218
218
|
return `Move the covering test under a declared coverage root, or add its root to coverage.coverageRoots in ark.config.json, then run Ark again.`;
|
|
219
|
+
case 'INVARIANT_CATALOG_EMPTY':
|
|
220
|
+
return typeof violation.file === 'string' && violation.file.length > 0
|
|
221
|
+
? `Add 1–2 short phrases to invariants[] in ${violation.file}, then run ark-check --doctor. Starters show the shape.`
|
|
222
|
+
: 'Add 1–2 short phrases to invariants[] in arkrules/<Domain>.json, then run ark-check --doctor. Starters show the shape.';
|
|
219
223
|
case 'ARKRULE_STRUCTURE':
|
|
220
224
|
case 'ARKRULE_INVARIANT':
|
|
221
225
|
case 'INVARIANT_UNCOVERED':
|
|
@@ -1168,9 +1168,17 @@ export function resolveCandidateFacts({
|
|
|
1168
1168
|
const arkOrderXiFieldWrites = [];
|
|
1169
1169
|
const arkOrderIngestWritesXi = [];
|
|
1170
1170
|
const arkOrderReleaseKeyCounts = [];
|
|
1171
|
-
const
|
|
1172
|
-
const
|
|
1173
|
-
const
|
|
1171
|
+
const arkRunActive = config.arkRun != null && typeof config.arkRun === 'object';
|
|
1172
|
+
const arkOrderActive = config.arkOrder != null && typeof config.arkOrder === 'object';
|
|
1173
|
+
const arkRulesActive =
|
|
1174
|
+
config.arkRules != null &&
|
|
1175
|
+
typeof config.arkRules === 'object' &&
|
|
1176
|
+
Object.keys(config.arkRules).length > 0;
|
|
1177
|
+
const xiKeys = arkOrderActive ? [...(config.arkOrder?.xiKeys ?? [])] : [];
|
|
1178
|
+
const compositionRootPatterns = arkRunActive
|
|
1179
|
+
? [...(config.arkRun?.compositionRoots ?? [])]
|
|
1180
|
+
: [];
|
|
1181
|
+
const planeRootPatterns = arkOrderActive ? [...(config.arkOrder?.planeRoots ?? [])] : [];
|
|
1174
1182
|
|
|
1175
1183
|
const seedPathSet = new Set(candidateFiles.map((file) => file.path));
|
|
1176
1184
|
const ingest = (candidate, fullExtract) => {
|
|
@@ -1256,60 +1264,67 @@ export function resolveCandidateFacts({
|
|
|
1256
1264
|
);
|
|
1257
1265
|
// Class-shape extraction is text-conservative (false negatives over false positives).
|
|
1258
1266
|
// Only TS/TSX candidates; sensors consume the same shape via facts.classShapes.
|
|
1267
|
+
// Extra-plane and ArkRules extractors stay silent when that plane is off (#212).
|
|
1259
1268
|
if (/\.(tsx?|mts|cts)$/i.test(candidate.path)) {
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
arkRunKernelCalls.push(
|
|
1267
|
-
...extractArkRunKernelCallsFromSource(candidate.path, candidate.content)
|
|
1268
|
-
);
|
|
1269
|
-
} catch {
|
|
1270
|
-
// Never fail the resolver for ArkRun call extraction.
|
|
1271
|
-
}
|
|
1272
|
-
try {
|
|
1273
|
-
arkRunDeclarations.push(
|
|
1274
|
-
...extractArkRunDeclarationsFromSource(candidate.path, candidate.content)
|
|
1275
|
-
);
|
|
1276
|
-
} catch {
|
|
1277
|
-
// Never fail the resolver for ArkRun declaration extraction.
|
|
1278
|
-
}
|
|
1279
|
-
try {
|
|
1280
|
-
arkOrderPlaneCalls.push(
|
|
1281
|
-
...extractArkOrderPlaneCallsFromSource(candidate.path, candidate.content)
|
|
1282
|
-
);
|
|
1283
|
-
} catch {
|
|
1284
|
-
// Never fail the resolver for ArkOrder factory extraction.
|
|
1285
|
-
}
|
|
1286
|
-
try {
|
|
1287
|
-
arkOrderGenericUpdates.push(
|
|
1288
|
-
...extractArkOrderGenericUpdatesFromSource(candidate.path, candidate.content)
|
|
1289
|
-
);
|
|
1290
|
-
} catch {
|
|
1291
|
-
// Never fail the resolver for ArkOrder generic-update extraction.
|
|
1292
|
-
}
|
|
1293
|
-
try {
|
|
1294
|
-
arkOrderXiFieldWrites.push(
|
|
1295
|
-
...extractArkOrderXiFieldWritesFromSource(candidate.path, candidate.content, xiKeys)
|
|
1296
|
-
);
|
|
1297
|
-
} catch {
|
|
1298
|
-
// Never fail the resolver for ArkOrder xi-field-write extraction.
|
|
1269
|
+
if (arkRulesActive || arkRunActive) {
|
|
1270
|
+
try {
|
|
1271
|
+
classShapes.push(...extractClassShapesFromSource(candidate.path, candidate.content));
|
|
1272
|
+
} catch {
|
|
1273
|
+
// Never fail the resolver for shape extraction; sensors stay silent on this file.
|
|
1274
|
+
}
|
|
1299
1275
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1276
|
+
if (arkRunActive) {
|
|
1277
|
+
try {
|
|
1278
|
+
arkRunKernelCalls.push(
|
|
1279
|
+
...extractArkRunKernelCallsFromSource(candidate.path, candidate.content)
|
|
1280
|
+
);
|
|
1281
|
+
} catch {
|
|
1282
|
+
// Never fail the resolver for ArkRun call extraction.
|
|
1283
|
+
}
|
|
1284
|
+
try {
|
|
1285
|
+
arkRunDeclarations.push(
|
|
1286
|
+
...extractArkRunDeclarationsFromSource(candidate.path, candidate.content)
|
|
1287
|
+
);
|
|
1288
|
+
} catch {
|
|
1289
|
+
// Never fail the resolver for ArkRun declaration extraction.
|
|
1290
|
+
}
|
|
1306
1291
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1292
|
+
if (arkOrderActive) {
|
|
1293
|
+
try {
|
|
1294
|
+
arkOrderPlaneCalls.push(
|
|
1295
|
+
...extractArkOrderPlaneCallsFromSource(candidate.path, candidate.content)
|
|
1296
|
+
);
|
|
1297
|
+
} catch {
|
|
1298
|
+
// Never fail the resolver for ArkOrder factory extraction.
|
|
1299
|
+
}
|
|
1300
|
+
try {
|
|
1301
|
+
arkOrderGenericUpdates.push(
|
|
1302
|
+
...extractArkOrderGenericUpdatesFromSource(candidate.path, candidate.content)
|
|
1303
|
+
);
|
|
1304
|
+
} catch {
|
|
1305
|
+
// Never fail the resolver for ArkOrder generic-update extraction.
|
|
1306
|
+
}
|
|
1307
|
+
try {
|
|
1308
|
+
arkOrderXiFieldWrites.push(
|
|
1309
|
+
...extractArkOrderXiFieldWritesFromSource(candidate.path, candidate.content, xiKeys)
|
|
1310
|
+
);
|
|
1311
|
+
} catch {
|
|
1312
|
+
// Never fail the resolver for ArkOrder xi-field-write extraction.
|
|
1313
|
+
}
|
|
1314
|
+
try {
|
|
1315
|
+
arkOrderIngestWritesXi.push(
|
|
1316
|
+
...extractArkOrderIngestWritesXiFromSource(candidate.path, candidate.content)
|
|
1317
|
+
);
|
|
1318
|
+
} catch {
|
|
1319
|
+
// Never fail the resolver for ArkOrder ingest-writes-ξ extraction.
|
|
1320
|
+
}
|
|
1321
|
+
try {
|
|
1322
|
+
arkOrderReleaseKeyCounts.push(
|
|
1323
|
+
...extractArkOrderReleaseKeyCountsFromSource(candidate.path, candidate.content)
|
|
1324
|
+
);
|
|
1325
|
+
} catch {
|
|
1326
|
+
// Never fail the resolver for ArkOrder release key-count extraction.
|
|
1327
|
+
}
|
|
1313
1328
|
}
|
|
1314
1329
|
}
|
|
1315
1330
|
};
|
|
@@ -1355,18 +1370,20 @@ export function resolveCandidateFacts({
|
|
|
1355
1370
|
? candidateFiles.filter((file) => seedPathSet.has(file.path))
|
|
1356
1371
|
: candidateFiles;
|
|
1357
1372
|
const admittedTypeNames = new Set(classShapes.map((shape) => shape.className));
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1373
|
+
if (arkRunActive) {
|
|
1374
|
+
for (const candidate of extractCandidates) {
|
|
1375
|
+
if (!/\.(tsx?|mts|cts)$/i.test(candidate.path)) continue;
|
|
1376
|
+
try {
|
|
1377
|
+
arkRunManagedNews.push(
|
|
1378
|
+
...extractArkRunManagedNewsFromSource(
|
|
1379
|
+
candidate.path,
|
|
1380
|
+
candidate.content,
|
|
1381
|
+
admittedTypeNames
|
|
1382
|
+
)
|
|
1383
|
+
);
|
|
1384
|
+
} catch {
|
|
1385
|
+
// Never fail the resolver for managed-new extraction.
|
|
1386
|
+
}
|
|
1370
1387
|
}
|
|
1371
1388
|
}
|
|
1372
1389
|
|