arkgate 4.8.24 → 4.8.25
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 +85 -0
- package/README.md +2 -2
- package/bin/ark-check-runtime.mjs +17 -14
- package/bin/ark-layer-match.mjs +115 -13
- package/bin/ark-mcp-runtime.mjs +36 -31
- package/bin/ark-shared.mjs +4 -23
- package/bin/lib/adoption-stance.mjs +2 -2
- package/bin/lib/adr-path.mjs +3 -3
- package/bin/lib/agent-gates.mjs +0 -61
- package/bin/lib/agent-homes.mjs +12 -21
- package/bin/lib/agent-projection-command.mjs +4 -4
- package/bin/lib/ambient-state.mjs +9 -30
- package/bin/lib/analysis-engine.mjs +14 -14
- package/bin/lib/architecture-scan.mjs +11 -9
- package/bin/lib/ark-order-doctor.mjs +3 -28
- package/bin/lib/ark-order-facts.mjs +6 -99
- package/bin/lib/ark-order-types.mjs +1 -2
- package/bin/lib/ark-run-doctor.mjs +3 -28
- package/bin/lib/ark-run-facts.mjs +30 -40
- package/bin/lib/ark-run-sensors.mjs +1 -1
- package/bin/lib/arkrule-file-hints.mjs +2 -47
- package/bin/lib/arkrules-contract.mjs +124 -133
- package/bin/lib/arkrules-sensors.mjs +7 -5
- package/bin/lib/check-args.mjs +63 -1
- package/bin/lib/ci-and-commands.mjs +8 -72
- package/bin/lib/ci-merge-boundary.mjs +1 -1
- package/bin/lib/class-source-scan.mjs +5 -5
- package/bin/lib/clone-detection.mjs +474 -0
- package/bin/lib/codex-home.mjs +1 -1
- package/bin/lib/config-contract-slices.mjs +44 -6
- package/bin/lib/config-contract.mjs +40 -117
- package/bin/lib/config-extras.mjs +16 -22
- package/bin/lib/contract-smells.mjs +33 -15
- package/bin/lib/core-ratchet.mjs +0 -2
- package/bin/lib/dashboard-cli.mjs +2 -2
- package/bin/lib/deep-module-coach.mjs +2 -4
- package/bin/lib/deploy-path.mjs +13 -38
- package/bin/lib/design-smells.mjs +2 -2
- package/bin/lib/diagnostic-catalog.mjs +8 -1
- package/bin/lib/doctor-advisories.mjs +55 -9
- package/bin/lib/doctor-green-cite.mjs +1 -1
- package/bin/lib/doctor-human.mjs +1 -1
- package/bin/lib/doctor-next-actions.mjs +2 -2
- package/bin/lib/doctor-plan.mjs +4 -6
- package/bin/lib/duplication-io.mjs +572 -0
- package/bin/lib/effective-contract-load.mjs +181 -44
- package/bin/lib/entry-points-io.mjs +424 -0
- package/bin/lib/extra-merge-teeth.mjs +36 -0
- package/bin/lib/field-install.mjs +38 -14
- package/bin/lib/first-run-help.mjs +7 -3
- package/bin/lib/flat-parent-importers.mjs +5 -3
- package/bin/lib/gate-files.mjs +30 -89
- package/bin/lib/github-enforcement.mjs +165 -28
- package/bin/lib/golden-pattern.mjs +1 -1
- package/bin/lib/hook-templates.mjs +16 -16
- package/bin/lib/html-report-advisories.mjs +4 -0
- package/bin/lib/html-report-depth.mjs +2 -2
- package/bin/lib/html-report.mjs +2 -2
- package/bin/lib/import-graph-projection.mjs +112 -0
- package/bin/lib/improvement-compass-doctor.mjs +1 -24
- package/bin/lib/invariant-coverage-io.mjs +4 -3
- package/bin/lib/invariant-coverage.mjs +26 -0
- package/bin/lib/invariant-probe-cli.mjs +347 -0
- package/bin/lib/invariant-probe-io.mjs +165 -0
- package/bin/lib/invariant-probe-runner.mjs +268 -0
- package/bin/lib/invariant-probe-sites.mjs +174 -0
- package/bin/lib/invariant-probe-workspace.mjs +183 -0
- package/bin/lib/invariant-probe.mjs +461 -0
- package/bin/lib/invariant-tests-path.mjs +16 -22
- package/bin/lib/literal-path-drift.mjs +4 -2
- package/bin/lib/managed-upgrade.mjs +2 -8
- package/bin/lib/mcp-adoption.mjs +4 -5
- package/bin/lib/mcp-hook-payload.mjs +4 -4
- package/bin/lib/orphan-modules-io.mjs +458 -0
- package/bin/lib/orphan-modules.mjs +251 -0
- package/bin/lib/outside-importers.mjs +197 -0
- package/bin/lib/package-manager.mjs +3 -3
- package/bin/lib/parse-health.mjs +1 -1
- package/bin/lib/persistence-write-hint.mjs +33 -8
- package/bin/lib/pin-dependent-gates.mjs +2 -2
- package/bin/lib/policy-delta-io.mjs +55 -8
- package/bin/lib/post-green-path.mjs +1 -1
- package/bin/lib/prepare-change.mjs +3 -9
- package/bin/lib/prepare-write.mjs +1 -2
- package/bin/lib/presets.mjs +56 -68
- package/bin/lib/product-copy.mjs +1 -6
- package/bin/lib/project-root.mjs +31 -62
- package/bin/lib/resolved-candidate-facts.mjs +90 -62
- package/bin/lib/rules-under-contract.mjs +72 -8
- package/bin/lib/scan-files.mjs +4 -4
- package/bin/lib/schema-validation.mjs +130 -0
- package/bin/lib/sensor-promote-cli.mjs +2 -11
- package/bin/lib/sensor-promote-io.mjs +33 -22
- package/bin/lib/sensor-promotion.mjs +1 -1
- package/bin/lib/skill-catalog-apply.mjs +1 -1
- package/bin/lib/skill-install.mjs +42 -38
- package/bin/lib/snippet-analysis.mjs +1 -1
- package/bin/lib/start-install-recovery.mjs +1 -1
- package/bin/lib/start-preview.mjs +1 -1
- package/bin/lib/states-transitions-presence.mjs +3 -3
- package/bin/lib/status-command.mjs +2 -18
- package/bin/lib/status-manifest.mjs +18 -26
- package/bin/lib/status-transition-catalog.mjs +0 -6
- package/bin/lib/suggestions.mjs +3 -3
- package/bin/lib/team-parliament-io.mjs +3 -17
- package/bin/lib/team-parliament.mjs +15 -9
- package/bin/lib/upgrade-behavior-changes.mjs +2 -2
- package/bin/lib/upgrade-command.mjs +2 -24
- package/bin/lib/upgrade-outcome.mjs +1 -1
- package/bin/lib/upstream-report.mjs +21 -10
- package/bin/lib/violations.mjs +10 -9
- package/bin/lib/weakest-link.mjs +13 -0
- package/bin/lib/write-path-capabilities.mjs +2 -2
- package/dist/chunk-AHACEKSV.js +16 -0
- package/dist/chunk-UN5EH4QV.cjs +16 -0
- package/dist/{configTypes-BJbaeoj4.d.cts → configTypes-GPvvdleM.d.cts} +12 -1
- package/dist/{configTypes-BJbaeoj4.d.ts → configTypes-GPvvdleM.d.ts} +12 -1
- package/dist/eslint/index.cjs +11 -11
- package/dist/eslint/index.d.cts +10 -1
- package/dist/eslint/index.d.ts +10 -1
- package/dist/eslint/index.js +11 -11
- package/dist/index.cjs +34 -34
- package/dist/index.d.cts +587 -515
- package/dist/index.d.ts +587 -515
- package/dist/index.js +34 -34
- package/dist/nestjs/index.cjs +1 -1
- package/dist/nestjs/index.d.cts +1 -1
- package/dist/nestjs/index.d.ts +1 -1
- package/dist/nestjs/index.js +1 -1
- package/dist/order/index.cjs +1 -1
- package/dist/order/index.js +1 -1
- package/dist/runtime/index.cjs +12 -12
- package/dist/runtime/index.d.cts +174 -136
- package/dist/runtime/index.d.ts +174 -136
- package/dist/runtime/index.js +12 -12
- package/dist/{types-DRinG-vT.d.cts → types-CnauMQQL.d.cts} +33 -25
- package/dist/{types-DRinG-vT.d.ts → types-CnauMQQL.d.ts} +33 -25
- package/docs/README.md +2 -1
- package/docs/agent-guide.md +134 -2
- package/docs/ai-gates.md +15 -1
- package/docs/configuration.md +40 -11
- package/docs/diagnostics.md +118 -2
- package/docs/package-surface.md +4 -1
- package/docs/product-voice.md +7 -1
- package/package.json +11 -2
- package/schemas/ark.config.schema.json +31 -3
- package/schemas/ark.invariant-probe.schema.json +368 -0
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +1 -0
- package/templates/agent-skills/ark-contract/SKILL.md +17 -0
- package/templates/agent-skills/ark-coverage/SKILL.md +2 -0
- package/templates/agent-skills/ark-explore/SKILL.md +7 -0
- package/templates/agent-skills/ark-fix/SKILL.md +20 -0
- package/templates/agent-skills/ark-place/SKILL.md +8 -0
- package/templates/skills/ark-adopt.md +1 -0
- package/templates/skills/ark-contract.md +17 -0
- package/templates/skills/ark-coverage.md +2 -0
- package/templates/skills/ark-explore.md +7 -0
- package/templates/skills/ark-fix.md +20 -0
- package/templates/skills/ark-place.md +8 -0
- package/bin/lib/ark-order-error.mjs +0 -18
- package/bin/lib/ark-order-invariants.mjs +0 -388
- package/bin/lib/stable-hash.mjs +0 -90
- package/dist/chunk-MBXCFLO4.js +0 -16
- package/dist/chunk-RNQHRDCQ.cjs +0 -16
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry evidence for the files-nothing-imports advisory (ADR 0037 D3).
|
|
3
|
+
*
|
|
4
|
+
* A closed list of sources, each with a `source` label: package.json fields
|
|
5
|
+
* and scripts (plus CI workflow run steps), framework conventions, the
|
|
6
|
+
* arkRun / arkOrder / slice-wall roots in ark.config.json, config and setup
|
|
7
|
+
* files (and the source files a config file names), ambient declaration files,
|
|
8
|
+
* and the optional `.ark/entry-points.json` sidecar. Reads are bounded. A file
|
|
9
|
+
* any source covers is never listed. Advisory input only.
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { detectWorkspaces, globToRegExp } from '../ark-shared.mjs';
|
|
14
|
+
|
|
15
|
+
export const ENTRY_POINTS_SIDECAR = '.ark/entry-points.json';
|
|
16
|
+
const SIDECAR_MAX_BYTES = 64 * 1024;
|
|
17
|
+
const SIDECAR_MAX_ENTRIES = 200;
|
|
18
|
+
const MAX_PACKAGES = 200;
|
|
19
|
+
const MAX_TEXT_BYTES = 256 * 1024;
|
|
20
|
+
const MAX_WORKFLOWS = 50;
|
|
21
|
+
const SOURCE_EXT = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
|
|
22
|
+
const SOURCE_FILE = /\.[cm]?[jt]sx?$/;
|
|
23
|
+
const DECLARATION_FILE = /\.d\.[cm]?ts$/;
|
|
24
|
+
const BUILT_DIRS = ['dist', 'build', 'lib', 'out'];
|
|
25
|
+
const CONFIG_FILE = /(?:^|\/)[^/]+\.config\.[cm]?[jt]s$/;
|
|
26
|
+
const SETUP_FILE = /(?:^|\/)(?:vitest\.setup|jest\.setup|setupTests)\.[cm]?[jt]sx?$/;
|
|
27
|
+
const NEXT_ROUTE =
|
|
28
|
+
/^(?:src\/)?app\/(?:.*\/)?(?:page|layout|template|loading|error|global-error|not-found|default|route|opengraph-image|twitter-image|icon|apple-icon|sitemap|robots|manifest)\.[cm]?[jt]sx?$/;
|
|
29
|
+
const NEXT_PAGES = /^(?:src\/)?pages\//;
|
|
30
|
+
const NEXT_ROOT_FILE = /^(?:src\/)?(?:middleware|proxy|instrumentation|instrumentation-client|mdx-components)\.[cm]?[jt]sx?$/;
|
|
31
|
+
const STORY_FILE = /\.stories\.[cm]?[jt]sx?$/;
|
|
32
|
+
const PATH_TOKEN = /[\w@.][\w@./-]*\.[cm]?[jt]sx?(?![\w/])/g;
|
|
33
|
+
const STRING_LITERAL = /(['"`])([^'"`\n]{1,300})\1/g;
|
|
34
|
+
const KEY_VALUE = /(?:(['"])([^'"\n]{1,200})\1|([A-Za-z_$][\w$]*))\s*:\s*(['"])([^'"\n]{1,300})\4/g;
|
|
35
|
+
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
36
|
+
|
|
37
|
+
function readText(absolute, max = MAX_TEXT_BYTES) {
|
|
38
|
+
try {
|
|
39
|
+
const stat = fs.statSync(absolute);
|
|
40
|
+
if (!stat.isFile() || stat.size > max) return null;
|
|
41
|
+
return fs.readFileSync(absolute, 'utf8');
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readJson(absolute) {
|
|
48
|
+
const text = readText(absolute);
|
|
49
|
+
if (text == null) return null;
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(text);
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** tsconfig is JSONC: drop comments and trailing commas before parsing. */
|
|
58
|
+
function readJsonc(absolute) {
|
|
59
|
+
const text = readText(absolute);
|
|
60
|
+
if (text == null) return null;
|
|
61
|
+
const stripped = text
|
|
62
|
+
.replace(/("(?:\\.|[^"\\])*")|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, (match, quoted) => quoted ?? '')
|
|
63
|
+
.replace(/,(\s*[}\]])/g, '$1');
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(stripped);
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Repo-relative, posix, no leading `./`; null when it leaves the root. */
|
|
72
|
+
function joinRel(base, rel) {
|
|
73
|
+
const raw = String(rel).replace(/\\/g, '/').replace(/^\/+/, '');
|
|
74
|
+
const joined = path.posix.normalize(base === '.' || base === '' ? raw : `${base}/${raw}`);
|
|
75
|
+
if (joined === '..' || joined.startsWith('../')) return null;
|
|
76
|
+
return joined.replace(/^\.\//, '');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function stripSourceExt(rel) {
|
|
80
|
+
return rel.replace(DECLARATION_FILE, '').replace(SOURCE_FILE, '');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Governed file for a path stem: exact, then with a source extension, then as a directory index. */
|
|
84
|
+
export function probeGoverned(stem, governed) {
|
|
85
|
+
if (!stem) return null;
|
|
86
|
+
if (governed.has(stem)) return stem;
|
|
87
|
+
for (const ext of SOURCE_EXT) if (governed.has(`${stem}${ext}`)) return `${stem}${ext}`;
|
|
88
|
+
for (const ext of SOURCE_EXT) if (governed.has(`${stem}/index${ext}`)) return `${stem}/index${ext}`;
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function packageRoots(root, governed) {
|
|
93
|
+
const roots = new Set(['.']);
|
|
94
|
+
let dirs = [];
|
|
95
|
+
try {
|
|
96
|
+
dirs = detectWorkspaces(root);
|
|
97
|
+
} catch {
|
|
98
|
+
dirs = [];
|
|
99
|
+
}
|
|
100
|
+
const hasGovernedUnder = (dir) => {
|
|
101
|
+
const prefix = `${dir}/`;
|
|
102
|
+
for (const file of governed) if (file.startsWith(prefix)) return true;
|
|
103
|
+
return false;
|
|
104
|
+
};
|
|
105
|
+
const add = (dir) => {
|
|
106
|
+
if (roots.size < MAX_PACKAGES && hasGovernedUnder(dir)) roots.add(dir);
|
|
107
|
+
};
|
|
108
|
+
for (const dir of dirs) {
|
|
109
|
+
const absolute = path.join(root, dir);
|
|
110
|
+
if (fs.existsSync(path.join(absolute, 'package.json'))) {
|
|
111
|
+
add(dir);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
let children = [];
|
|
115
|
+
try {
|
|
116
|
+
children = fs.readdirSync(absolute, { withFileTypes: true });
|
|
117
|
+
} catch {
|
|
118
|
+
children = [];
|
|
119
|
+
}
|
|
120
|
+
for (const child of children) {
|
|
121
|
+
if (!child.isDirectory() || child.name.startsWith('.') || child.name === 'node_modules') continue;
|
|
122
|
+
if (fs.existsSync(path.join(absolute, child.name, 'package.json'))) add(`${dir}/${child.name}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return [...roots].sort();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function stringLeaves(value, out = []) {
|
|
129
|
+
if (typeof value === 'string') out.push(value);
|
|
130
|
+
else if (Array.isArray(value)) for (const item of value) stringLeaves(item, out);
|
|
131
|
+
else if (value && typeof value === 'object') for (const item of Object.values(value)) stringLeaves(item, out);
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function tsOutput(root, pkgRoot, ts) {
|
|
136
|
+
const file = path.join(root, pkgRoot, 'tsconfig.json');
|
|
137
|
+
let options;
|
|
138
|
+
if (ts?.readConfigFile && ts?.sys?.readFile) {
|
|
139
|
+
options = ts.readConfigFile(file, ts.sys.readFile)?.config?.compilerOptions;
|
|
140
|
+
} else {
|
|
141
|
+
options = readJsonc(file)?.compilerOptions;
|
|
142
|
+
}
|
|
143
|
+
const clean = (value) =>
|
|
144
|
+
typeof value === 'string' && value.length > 0 ? value.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '') : null;
|
|
145
|
+
return { outDir: clean(options?.outDir), rootDir: clean(options?.rootDir) };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Config files at a package root: their literals name entries and built-name maps. */
|
|
149
|
+
function scanConfigFiles(root, pkgRoot, governed) {
|
|
150
|
+
const named = new Set();
|
|
151
|
+
const outNames = new Map();
|
|
152
|
+
let entries = [];
|
|
153
|
+
try {
|
|
154
|
+
entries = fs.readdirSync(path.join(root, pkgRoot), { withFileTypes: true });
|
|
155
|
+
} catch {
|
|
156
|
+
entries = [];
|
|
157
|
+
}
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
if (!entry.isFile() || !CONFIG_FILE.test(entry.name)) continue;
|
|
160
|
+
const text = readText(path.join(root, pkgRoot, entry.name));
|
|
161
|
+
if (text == null) continue;
|
|
162
|
+
for (const match of text.matchAll(KEY_VALUE)) {
|
|
163
|
+
const key = match[2] ?? match[3];
|
|
164
|
+
const target = probeGoverned(stripSourceExt(joinRel(pkgRoot, match[5]) ?? ''), governed);
|
|
165
|
+
if (key && target && !outNames.has(key)) outNames.set(key, target);
|
|
166
|
+
}
|
|
167
|
+
for (const match of text.matchAll(STRING_LITERAL)) {
|
|
168
|
+
const literal = match[2];
|
|
169
|
+
if (!literal.includes('/') && !SOURCE_FILE.test(literal)) continue;
|
|
170
|
+
const target = probeGoverned(stripSourceExt(joinRel(pkgRoot, literal) ?? ''), governed);
|
|
171
|
+
if (!target) continue;
|
|
172
|
+
named.add(target);
|
|
173
|
+
const stem = path.posix.basename(stripSourceExt(target));
|
|
174
|
+
if (!outNames.has(stem)) outNames.set(stem, target);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { named, outNames };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Built path (`dist/x.js`) or source path → governed source.
|
|
182
|
+
* @returns {{ path: string } | { unmapped: string } | null} null = not ours to judge
|
|
183
|
+
*/
|
|
184
|
+
function mapToSource(rel, ctx) {
|
|
185
|
+
if (rel.includes('*')) return { unmapped: 'pattern export' };
|
|
186
|
+
const ext = path.posix.extname(rel);
|
|
187
|
+
if (ext && !SOURCE_FILE.test(rel) && !DECLARATION_FILE.test(rel)) return null;
|
|
188
|
+
if (ctx.governed.has(rel)) return { path: rel };
|
|
189
|
+
const direct = probeGoverned(stripSourceExt(rel), ctx.governed);
|
|
190
|
+
if (direct) return { path: direct };
|
|
191
|
+
const builtDirs = [...new Set([ctx.outDir, ...BUILT_DIRS].filter(Boolean))];
|
|
192
|
+
for (const built of builtDirs) {
|
|
193
|
+
const prefix = `${joinRel(ctx.pkgRoot, built)}/`;
|
|
194
|
+
if (!rel.startsWith(prefix)) continue;
|
|
195
|
+
const rest = stripSourceExt(rel.slice(prefix.length));
|
|
196
|
+
const viaBundler = ctx.outNames.get(rest);
|
|
197
|
+
if (viaBundler) return { path: viaBundler };
|
|
198
|
+
if (built === ctx.outDir && ctx.rootDir) {
|
|
199
|
+
const viaTsconfig = probeGoverned(joinRel(ctx.pkgRoot, `${ctx.rootDir}/${rest}`), ctx.governed);
|
|
200
|
+
if (viaTsconfig) return { path: viaTsconfig };
|
|
201
|
+
}
|
|
202
|
+
const viaSrc = probeGoverned(joinRel(ctx.pkgRoot, `src/${rest}`), ctx.governed);
|
|
203
|
+
if (viaSrc) return { path: viaSrc };
|
|
204
|
+
return { unmapped: 'built path maps to no governed source file' };
|
|
205
|
+
}
|
|
206
|
+
if (fs.existsSync(path.join(ctx.root, rel))) return null;
|
|
207
|
+
return { unmapped: 'path does not exist' };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function dependencyNames(pkg) {
|
|
211
|
+
return new Set(
|
|
212
|
+
[pkg?.dependencies, pkg?.devDependencies, pkg?.peerDependencies]
|
|
213
|
+
.flatMap((deps) => (deps && typeof deps === 'object' ? Object.keys(deps) : []))
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function viteEntries(root, pkgRoot, governed) {
|
|
218
|
+
const html = readText(path.join(root, pkgRoot, 'index.html'));
|
|
219
|
+
if (html == null) return [];
|
|
220
|
+
const out = [];
|
|
221
|
+
for (const tag of html.matchAll(/<script\b[^>]*>/gi)) {
|
|
222
|
+
if (!/type\s*=\s*["']module["']/i.test(tag[0])) continue;
|
|
223
|
+
const src = /src\s*=\s*["']([^"']+)["']/i.exec(tag[0])?.[1];
|
|
224
|
+
const target = src ? probeGoverned(stripSourceExt(joinRel(pkgRoot, src) ?? ''), governed) : null;
|
|
225
|
+
if (target) out.push(target);
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function nestEntry(root, pkgRoot, governed) {
|
|
231
|
+
const cli = readJson(path.join(root, pkgRoot, 'nest-cli.json'));
|
|
232
|
+
const sourceRoot = typeof cli?.sourceRoot === 'string' ? cli.sourceRoot : 'src';
|
|
233
|
+
const entryFile = typeof cli?.entryFile === 'string' ? cli.entryFile : 'main';
|
|
234
|
+
return probeGoverned(joinRel(pkgRoot, `${sourceRoot}/${entryFile}`), governed);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function workflowTexts(root) {
|
|
238
|
+
const dir = path.join(root, '.github', 'workflows');
|
|
239
|
+
let names = [];
|
|
240
|
+
try {
|
|
241
|
+
names = fs.readdirSync(dir).filter((name) => /\.ya?ml$/.test(name)).sort().slice(0, MAX_WORKFLOWS);
|
|
242
|
+
} catch {
|
|
243
|
+
names = [];
|
|
244
|
+
}
|
|
245
|
+
return names.map((name) => readText(path.join(dir, name)) ?? '');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function arkConfigGlobs(config) {
|
|
249
|
+
const globs = [
|
|
250
|
+
...(config?.arkRun?.kernelRoots ?? []),
|
|
251
|
+
...(config?.arkRun?.compositionRoots ?? []),
|
|
252
|
+
...(config?.arkOrder?.planeRoots ?? []),
|
|
253
|
+
];
|
|
254
|
+
for (const rule of Array.isArray(config?.rules) ? config.rules : []) {
|
|
255
|
+
const stopAt = rule?.sharedImportsSlice?.stopAt;
|
|
256
|
+
if (Array.isArray(stopAt)) globs.push(...stopAt);
|
|
257
|
+
}
|
|
258
|
+
return globs.filter((glob) => typeof glob === 'string' && glob.length > 0);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function compileGlob(glob) {
|
|
262
|
+
try {
|
|
263
|
+
return globToRegExp(glob);
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Optional user-declared entries. Malformed → suppresses nothing; a line says so.
|
|
271
|
+
* @param {string} root
|
|
272
|
+
* @param {string} today YYYY-MM-DD, supplied by the caller (no clock here)
|
|
273
|
+
*/
|
|
274
|
+
export function loadEntryPointsSidecar(root, today) {
|
|
275
|
+
const absolute = path.join(root, ENTRY_POINTS_SIDECAR);
|
|
276
|
+
let stat;
|
|
277
|
+
try {
|
|
278
|
+
stat = fs.statSync(absolute);
|
|
279
|
+
} catch {
|
|
280
|
+
return { status: 'absent', active: [], expired: [] };
|
|
281
|
+
}
|
|
282
|
+
const malformed = (why) => ({
|
|
283
|
+
status: 'malformed',
|
|
284
|
+
active: [],
|
|
285
|
+
expired: [],
|
|
286
|
+
note: `${ENTRY_POINTS_SIDECAR} was not used (${why}). It suppresses nothing until it is fixed.`,
|
|
287
|
+
});
|
|
288
|
+
if (!stat.isFile() || stat.size > SIDECAR_MAX_BYTES) return malformed('larger than 64 KiB');
|
|
289
|
+
const doc = readJson(absolute);
|
|
290
|
+
if (!doc || doc.schemaVersion !== '1' || !Array.isArray(doc.entryPoints)) {
|
|
291
|
+
return malformed('expected { "schemaVersion": "1", "entryPoints": [...] }');
|
|
292
|
+
}
|
|
293
|
+
if (doc.entryPoints.length > SIDECAR_MAX_ENTRIES) return malformed('more than 200 entries');
|
|
294
|
+
const active = [];
|
|
295
|
+
const expired = [];
|
|
296
|
+
for (const row of doc.entryPoints) {
|
|
297
|
+
const glob = typeof row?.glob === 'string' ? row.glob.trim() : '';
|
|
298
|
+
const reason = typeof row?.reason === 'string' ? row.reason.trim() : '';
|
|
299
|
+
const reviewBy = row?.reviewBy;
|
|
300
|
+
if (!glob || !reason || (reviewBy !== undefined && (typeof reviewBy !== 'string' || !DATE.test(reviewBy)))) {
|
|
301
|
+
return malformed('every entry needs a glob and a reason; reviewBy is YYYY-MM-DD');
|
|
302
|
+
}
|
|
303
|
+
if (reviewBy !== undefined && typeof today === 'string' && reviewBy < today) expired.push({ glob, reason, reviewBy });
|
|
304
|
+
else active.push({ glob, reason, ...(reviewBy ? { reviewBy } : {}) });
|
|
305
|
+
}
|
|
306
|
+
return { status: 'loaded', active, expired };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* @param {string} root
|
|
311
|
+
* @param {{ governed: string[], config?: object, ts?: object, today?: string }} input
|
|
312
|
+
* @returns {{
|
|
313
|
+
* entries: Map<string, string>,
|
|
314
|
+
* unmapped: Array<{ declared: string, source: string, reason: string }>,
|
|
315
|
+
* frameworks: string[],
|
|
316
|
+
* sidecar: ReturnType<typeof loadEntryPointsSidecar>,
|
|
317
|
+
* expiredByPath: Map<string, string[]>,
|
|
318
|
+
* }}
|
|
319
|
+
*/
|
|
320
|
+
export function collectEntryPoints(root, input) {
|
|
321
|
+
const governed = new Set(input.governed);
|
|
322
|
+
const entries = new Map();
|
|
323
|
+
const unmapped = [];
|
|
324
|
+
const frameworks = new Set();
|
|
325
|
+
const cover = (file, source) => {
|
|
326
|
+
if (file && governed.has(file) && !entries.has(file)) entries.set(file, source);
|
|
327
|
+
};
|
|
328
|
+
const workflows = workflowTexts(root);
|
|
329
|
+
|
|
330
|
+
for (const pkgRoot of packageRoots(root, governed)) {
|
|
331
|
+
const pkg = readJson(path.join(root, pkgRoot, 'package.json'));
|
|
332
|
+
const { named, outNames } = scanConfigFiles(root, pkgRoot, governed);
|
|
333
|
+
const ctx = { root, pkgRoot, governed, outNames, ...tsOutput(root, pkgRoot, input.ts) };
|
|
334
|
+
if (pkg) {
|
|
335
|
+
const fields = [pkg.main, pkg.module, pkg.types, pkg.typings, pkg.browser, pkg.bin, pkg.exports];
|
|
336
|
+
for (const leaf of stringLeaves(fields)) {
|
|
337
|
+
const rel = joinRel(pkgRoot, leaf);
|
|
338
|
+
const mapped = rel ? mapToSource(rel, ctx) : null;
|
|
339
|
+
if (mapped?.path) cover(mapped.path, 'package-json');
|
|
340
|
+
else if (mapped?.unmapped) unmapped.push({ declared: rel, source: 'package-json', reason: mapped.unmapped });
|
|
341
|
+
}
|
|
342
|
+
for (const script of stringLeaves(pkg.scripts ?? {})) {
|
|
343
|
+
for (const token of script.match(PATH_TOKEN) ?? []) {
|
|
344
|
+
const rel = joinRel(pkgRoot, token);
|
|
345
|
+
const mapped = rel ? mapToSource(rel, ctx) : null;
|
|
346
|
+
if (mapped?.path) cover(mapped.path, 'package-scripts');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (pkgRoot === '.') {
|
|
351
|
+
for (const text of workflows) {
|
|
352
|
+
for (const token of text.match(PATH_TOKEN) ?? []) {
|
|
353
|
+
const rel = joinRel('.', token);
|
|
354
|
+
const mapped = rel ? mapToSource(rel, ctx) : null;
|
|
355
|
+
if (mapped?.path) cover(mapped.path, 'package-scripts');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for (const file of named) cover(file, 'config-file');
|
|
360
|
+
|
|
361
|
+
const deps = dependencyNames(pkg);
|
|
362
|
+
const prefix = pkgRoot === '.' ? '' : `${pkgRoot}/`;
|
|
363
|
+
const local = (file) => (file.startsWith(prefix) ? file.slice(prefix.length) : null);
|
|
364
|
+
const nextConfig = ['js', 'mjs', 'cjs', 'ts', 'mts'].some((ext) =>
|
|
365
|
+
fs.existsSync(path.join(root, pkgRoot, `next.config.${ext}`))
|
|
366
|
+
);
|
|
367
|
+
if (deps.has('next') || nextConfig) {
|
|
368
|
+
frameworks.add('next');
|
|
369
|
+
for (const file of governed) {
|
|
370
|
+
const rest = local(file);
|
|
371
|
+
if (rest && (NEXT_ROUTE.test(rest) || NEXT_PAGES.test(rest) || NEXT_ROOT_FILE.test(rest))) cover(file, 'framework');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const vite = viteEntries(root, pkgRoot, governed);
|
|
375
|
+
if (vite.length > 0 || deps.has('vite')) frameworks.add('vite');
|
|
376
|
+
for (const file of vite) cover(file, 'framework');
|
|
377
|
+
if (deps.has('@nestjs/core')) {
|
|
378
|
+
frameworks.add('nest');
|
|
379
|
+
cover(nestEntry(root, pkgRoot, governed), 'framework');
|
|
380
|
+
}
|
|
381
|
+
if ([...deps].some((name) => name === 'storybook' || name.startsWith('@storybook/'))) frameworks.add('storybook');
|
|
382
|
+
}
|
|
383
|
+
if (fs.existsSync(path.join(root, 'vercel.json'))) {
|
|
384
|
+
frameworks.add('vercel');
|
|
385
|
+
for (const file of governed) if (file.startsWith('api/')) cover(file, 'framework');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const arkGlobs = arkConfigGlobs(input.config).map(compileGlob).filter(Boolean);
|
|
389
|
+
const sidecar = loadEntryPointsSidecar(root, input.today);
|
|
390
|
+
const active = sidecar.active.map((row) => compileGlob(row.glob)).filter(Boolean);
|
|
391
|
+
const expired = sidecar.expired.map((row) => ({ row, re: compileGlob(row.glob) })).filter((item) => item.re);
|
|
392
|
+
const expiredByPath = new Map();
|
|
393
|
+
for (const file of governed) {
|
|
394
|
+
if (STORY_FILE.test(file)) cover(file, 'framework');
|
|
395
|
+
if (CONFIG_FILE.test(file) || SETUP_FILE.test(file)) cover(file, 'config-file');
|
|
396
|
+
if (arkGlobs.some((re) => re.test(file))) cover(file, 'ark-config');
|
|
397
|
+
if (active.some((re) => re.test(file))) cover(file, 'sidecar');
|
|
398
|
+
for (const { row, re } of expired) {
|
|
399
|
+
if (!re.test(file)) continue;
|
|
400
|
+
const notes = expiredByPath.get(file) ?? [];
|
|
401
|
+
notes.push(`its ${ENTRY_POINTS_SIDECAR} entry (${row.glob}) passed reviewBy ${row.reviewBy}`);
|
|
402
|
+
expiredByPath.set(file, notes);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
unmapped.sort((left, right) => (left.declared < right.declared ? -1 : left.declared > right.declared ? 1 : 0));
|
|
406
|
+
return { entries, unmapped, frameworks: [...frameworks].sort(), sidecar, expiredByPath };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Ambient declaration files (`declare global` / `declare module`), checked lexically. */
|
|
410
|
+
export function ambientEntries(root, candidates) {
|
|
411
|
+
const out = [];
|
|
412
|
+
for (const rel of candidates) {
|
|
413
|
+
const text = readText(path.join(root, rel));
|
|
414
|
+
if (text && /\bdeclare\s+(?:global\b|module\s+['"])/.test(text)) out.push(rel);
|
|
415
|
+
}
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Entry counts per source label, for the evidence line. */
|
|
420
|
+
export function entriesBySource(entries) {
|
|
421
|
+
const bySource = {};
|
|
422
|
+
for (const source of entries.values()) bySource[source] = (bySource[source] ?? 0) + 1;
|
|
423
|
+
return bySource;
|
|
424
|
+
}
|
|
@@ -81,6 +81,42 @@ export function demoteExtraPlaneTeethUnderClassificationFloor(violations, classi
|
|
|
81
81
|
}
|
|
82
82
|
/** Stamp for extra-plane honesty: never one architecture score. */
|
|
83
83
|
export const MERGE_PLANES_DUAL_STAMP = 'Structure = heuristics; invariants = catalog+coverage evidence (not business runtime); ArkRun = kernel usage + declarations (not a score); ArkOrder = the few big product choices (not a score). Extra planes never merge into one architecture score. Advisory ArkRules ≠ merge teeth. Advisory ArkRun ≠ merge teeth. Advisory ArkOrder ≠ merge teeth.';
|
|
84
|
+
/**
|
|
85
|
+
* Doctor callers pass loosely typed ArkRules counts; the plane is active only
|
|
86
|
+
* when `active` is literally true. Counts pass through for countOrZero.
|
|
87
|
+
*/
|
|
88
|
+
export function mergePlanesArkRulesInput(arkRules) {
|
|
89
|
+
return {
|
|
90
|
+
active: arkRules?.active === true,
|
|
91
|
+
structureEnforced: arkRules?.structureEnforced,
|
|
92
|
+
structureTotal: arkRules?.structureTotal,
|
|
93
|
+
structureAdvisory: arkRules?.structureAdvisory,
|
|
94
|
+
invariantEnforced: arkRules?.invariantEnforced,
|
|
95
|
+
invariantTotal: arkRules?.invariantTotal,
|
|
96
|
+
invariantAdvisory: arkRules?.invariantAdvisory,
|
|
97
|
+
covered: arkRules?.covered,
|
|
98
|
+
uncovered: arkRules?.uncovered,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Thin status slice — counts only; residual null means unknown, not green. */
|
|
102
|
+
export function projectExtraPlaneStatus(input = {}) {
|
|
103
|
+
const present = input.present === true;
|
|
104
|
+
const mode = input.mode === 'enforced' || input.mode === 'advisory' ? input.mode : null;
|
|
105
|
+
const residualRaw = input.residual;
|
|
106
|
+
let residual = null;
|
|
107
|
+
if (typeof residualRaw === 'number' && Number.isFinite(residualRaw) && residualRaw >= 0) {
|
|
108
|
+
residual = Math.floor(residualRaw);
|
|
109
|
+
}
|
|
110
|
+
if (!present)
|
|
111
|
+
residual = residual == null ? 0 : residual;
|
|
112
|
+
return {
|
|
113
|
+
notAScore: true,
|
|
114
|
+
present,
|
|
115
|
+
mode: present ? mode : null,
|
|
116
|
+
extraMergeTeeth: present && mode === 'enforced' && input.extraMergeTeeth === true,
|
|
117
|
+
residual,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
84
120
|
function countOrZero(value) {
|
|
85
121
|
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
86
122
|
}
|
|
@@ -40,21 +40,41 @@ function matchingBrace(text, openIndex) {
|
|
|
40
40
|
return -1;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Insert `"name": value` into a top-level package.json object section (created at
|
|
45
|
+
* the end of the root object when absent) while preserving indentation, line
|
|
46
|
+
* endings, and single-line vs multi-line layout. Never replaces an existing key;
|
|
47
|
+
* callers check for it first.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} source package.json text
|
|
50
|
+
* @param {string} section top-level key, e.g. `scripts` or `devDependencies`
|
|
51
|
+
* @param {string} name property name
|
|
52
|
+
* @param {string} value property value (JSON-encoded here)
|
|
53
|
+
* @param {string} [unbalancedLabel] error label when the section's braces do not close
|
|
54
|
+
*/
|
|
55
|
+
export function addPackageJsonPropertyPreservingFormat(
|
|
56
|
+
source,
|
|
57
|
+
section,
|
|
58
|
+
name,
|
|
59
|
+
value,
|
|
60
|
+
unbalancedLabel = section
|
|
61
|
+
) {
|
|
44
62
|
const multiline = /\r?\n/.test(source);
|
|
45
63
|
const eol = source.includes('\r\n') ? '\r\n' : '\n';
|
|
46
64
|
const rootPropertyIndent = source.match(/\r?\n([ \t]+)"[^"\n]+"\s*:/)?.[1] ?? ' ';
|
|
47
65
|
const indentUnit = rootPropertyIndent;
|
|
48
|
-
const encoded = JSON.stringify(
|
|
49
|
-
const
|
|
66
|
+
const encoded = JSON.stringify(value);
|
|
67
|
+
const key = JSON.stringify(name);
|
|
68
|
+
const sectionKey = JSON.stringify(section);
|
|
69
|
+
const sectionMatch = new RegExp(`${sectionKey}\\s*:\\s*\\{`).exec(source);
|
|
50
70
|
|
|
51
|
-
if (
|
|
52
|
-
const open = source.indexOf('{',
|
|
71
|
+
if (sectionMatch) {
|
|
72
|
+
const open = source.indexOf('{', sectionMatch.index);
|
|
53
73
|
const close = matchingBrace(source, open);
|
|
54
|
-
if (close === -1) throw new Error(
|
|
74
|
+
if (close === -1) throw new Error(`Unbalanced ${unbalancedLabel} object`);
|
|
55
75
|
const body = source.slice(open + 1, close);
|
|
56
76
|
if (!multiline) {
|
|
57
|
-
const addition = body.trim() ?
|
|
77
|
+
const addition = body.trim() ? `,${key}:${encoded}` : `${key}:${encoded}`;
|
|
58
78
|
return `${source.slice(0, close)}${addition}${source.slice(close)}`;
|
|
59
79
|
}
|
|
60
80
|
const beforeClose = source.slice(0, close);
|
|
@@ -63,8 +83,8 @@ function addDevDependencyPreservingFormat(source, version) {
|
|
|
63
83
|
const closingIndent = trailing.slice(trailing.lastIndexOf('\n') + 1);
|
|
64
84
|
const propertyIndent = `${closingIndent}${indentUnit}`;
|
|
65
85
|
const addition = body.trim()
|
|
66
|
-
? `,${eol}${propertyIndent}
|
|
67
|
-
: `${propertyIndent}
|
|
86
|
+
? `,${eol}${propertyIndent}${key}: ${encoded}`
|
|
87
|
+
: `${propertyIndent}${key}: ${encoded}`;
|
|
68
88
|
return `${source.slice(0, contentEnd)}${addition}${eol}${closingIndent}${source.slice(close)}`;
|
|
69
89
|
}
|
|
70
90
|
|
|
@@ -73,16 +93,20 @@ function addDevDependencyPreservingFormat(source, version) {
|
|
|
73
93
|
const rootBody = source.slice(0, rootClose);
|
|
74
94
|
if (!multiline) {
|
|
75
95
|
const separator = rootBody.trim().endsWith('{') ? '' : ',';
|
|
76
|
-
return `${rootBody}${separator}
|
|
96
|
+
return `${rootBody}${separator}${sectionKey}:{${key}:${encoded}}${source.slice(rootClose)}`;
|
|
77
97
|
}
|
|
78
98
|
const trailing = rootBody.match(/\s*$/)?.[0] ?? '';
|
|
79
99
|
const contentEnd = rootClose - trailing.length;
|
|
80
100
|
const rootClosingIndent = trailing.slice(trailing.lastIndexOf('\n') + 1);
|
|
81
101
|
const separator = source.slice(0, contentEnd).trimEnd().endsWith('{') ? '' : ',';
|
|
82
|
-
const addition = `${separator}${eol}${rootPropertyIndent}
|
|
102
|
+
const addition = `${separator}${eol}${rootPropertyIndent}${sectionKey}: {${eol}${rootPropertyIndent}${indentUnit}${key}: ${encoded}${eol}${rootPropertyIndent}}`;
|
|
83
103
|
return `${source.slice(0, contentEnd)}${addition}${eol}${rootClosingIndent}${source.slice(rootClose)}`;
|
|
84
104
|
}
|
|
85
105
|
|
|
106
|
+
function addDevDependencyPreservingFormat(source, version) {
|
|
107
|
+
return addPackageJsonPropertyPreservingFormat(source, 'devDependencies', 'arkgate', version);
|
|
108
|
+
}
|
|
109
|
+
|
|
86
110
|
function replaceArkgateDependencyPreservingFormat(source, version) {
|
|
87
111
|
const encoded = JSON.stringify(version);
|
|
88
112
|
const next = source.replace(/("arkgate"\s*:\s*)(?:"(?:\\.|[^"\\])*")/, `$1${encoded}`);
|
|
@@ -126,7 +150,7 @@ const YAML_CONCURRENCY_GROUP_RE = /^\s*group:\s+/;
|
|
|
126
150
|
* True when the line invokes ark-check / arkgate-check (npx/pnpm/yarn/npm/node/run).
|
|
127
151
|
* YAML concurrency.group and job-id keys that only contain the name are not invocations.
|
|
128
152
|
*/
|
|
129
|
-
|
|
153
|
+
function isArkCheckInvocationLine(command) {
|
|
130
154
|
if (typeof command !== 'string' || !command.trim()) return false;
|
|
131
155
|
if (/^\s*#/.test(command)) return false;
|
|
132
156
|
if (YAML_CHECK_JOB_ID_RE.test(command)) return false;
|
|
@@ -258,7 +282,7 @@ export function syncBaselineIntoCheckSurfaces(root, opts = {}) {
|
|
|
258
282
|
* @param {string} root
|
|
259
283
|
* @returns {string|null}
|
|
260
284
|
*/
|
|
261
|
-
|
|
285
|
+
function readDeclaredArkgatePin(root) {
|
|
262
286
|
const pkgPath = path.join(root, 'package.json');
|
|
263
287
|
if (!fs.existsSync(pkgPath)) return null;
|
|
264
288
|
try {
|
|
@@ -278,7 +302,7 @@ export function readDeclaredArkgatePin(root) {
|
|
|
278
302
|
* Consumers never match: they depend on arkgate; they are not named arkgate with bin/.
|
|
279
303
|
* @param {string} root
|
|
280
304
|
*/
|
|
281
|
-
|
|
305
|
+
function isArkgateSelfHostRoot(root) {
|
|
282
306
|
try {
|
|
283
307
|
const pkgPath = path.join(root, 'package.json');
|
|
284
308
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
@@ -111,16 +111,16 @@ Non-interactive (no TTY): uses the same defaults as --yes — never calls readli
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/** `--sensors` is contract + coverage-evidence only — never a full-check pass. */
|
|
114
|
-
|
|
114
|
+
const SENSORS_PARTIAL_MODE_LINE =
|
|
115
115
|
'Contract + coverage-evidence only: no TypeScript, no analysis. Not a validity verdict.';
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
const SENSORS_DID_NOT_RUN = Object.freeze(['TypeScript', 'analysis']);
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
120
|
* Stamp a successful `--sensors --json` payload so agents cannot read exit 0 as
|
|
121
121
|
* a full-check pass. Failure payloads (`sensors.ok === false`) stay untouched.
|
|
122
122
|
*/
|
|
123
|
-
|
|
123
|
+
function stampSensorsPartialModePayload(payload) {
|
|
124
124
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return payload;
|
|
125
125
|
const sensors = payload.sensors;
|
|
126
126
|
if (!sensors || typeof sensors !== 'object' || Array.isArray(sensors)) return payload;
|
|
@@ -212,6 +212,10 @@ export function checkUsageAll() {
|
|
|
212
212
|
' what enforcing would cost: the findings each advisory rule already produces, from ONE run rather than one run per attempt.',
|
|
213
213
|
' Plan by default; --promote <ruleId> --apply (or --promote=<ruleId>) writes mode "enforced" into the ArkRules file that declares it.',
|
|
214
214
|
' A cost the run could not measure (incomplete analysis, classification floor) is named and --apply refuses. Exit 0 / 1 refused / 2 bad args.',
|
|
215
|
+
' ark-check --probe-invariants[=<id>] [--write] [--json] [--runner vitest|jest|node]',
|
|
216
|
+
' breaks each invariant\'s coverage.symbol on purpose (at most 3 small changes) in a temporary copy and runs only its covering tests',
|
|
217
|
+
' with your own runner. Not a score; never part of a check. --write saves .ark/invariant-probe.json: a fresh survived or',
|
|
218
|
+
' not-reached row refuses promotion. Your files are never changed. Exit 0 caught / 1 survived or not reached / 2 could not run.',
|
|
215
219
|
' ark-check --recommend [--json] [--write-plan] application-shape plan; --write-plan emits ark-adoption-plan.json',
|
|
216
220
|
' ark-check --list-policy-packs enthusiast packs (hexagonal, layered, feature-sliced, monorepo, ui-surface, vertical-slice, ddd-bounded-contexts)',
|
|
217
221
|
' ark-check --apply-policy-pack <id> [--force] write ark.config.json from templates/policy-packs/ (uses preset factory)',
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Importer graph for the flat-parent doctor suggestion.
|
|
3
3
|
*
|
|
4
|
-
* Prefers the
|
|
5
|
-
* paths included). Without
|
|
4
|
+
* Prefers the importer index the scan projected from resolved facts (ADR 0037),
|
|
5
|
+
* then raw resolved dependency facts (tsconfig paths included). Without either it scans the text and resolves relative
|
|
6
6
|
* specifiers plus tsconfig path aliases, so an `@/…` importer is never
|
|
7
7
|
* invisible. Advisory input only.
|
|
8
8
|
*/
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
+
import { importerGraphFromIndex } from './import-graph-projection.mjs';
|
|
11
12
|
import { readTsconfigAliases, resolveSpecifierToRel } from './import-resolve.mjs';
|
|
12
13
|
|
|
13
14
|
const SPEC_MARKS = [' from "', " from '", ' from `', 'require("', "require('", 'import("', "import('"];
|
|
@@ -36,7 +37,7 @@ function withoutComments(text) {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/** Every string module specifier in `text` (relative and bare). */
|
|
39
|
-
|
|
40
|
+
function moduleSpecifiers(text) {
|
|
40
41
|
const specs = [];
|
|
41
42
|
const source = withoutComments(String(text));
|
|
42
43
|
let i = 0;
|
|
@@ -112,6 +113,7 @@ function addEdge(graph, target, importer) {
|
|
|
112
113
|
* @param {{ root: string, files: string[], facts?: object, ts?: object }} input
|
|
113
114
|
*/
|
|
114
115
|
export function flatParentImporterGraph({ root, files, facts, ts }) {
|
|
116
|
+
if (facts?.importGraph) return importerGraphFromIndex(facts.importGraph);
|
|
115
117
|
const graph = new Map();
|
|
116
118
|
const fileSet = new Set(files);
|
|
117
119
|
const dependencies = Array.isArray(facts?.dependencies) ? facts.dependencies : null;
|