arkgate 4.8.23 → 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 +153 -0
- package/README.md +7 -5
- package/bin/ark-check-runtime.mjs +41 -91
- package/bin/ark-dashboard.mjs +56 -53
- package/bin/ark-layer-match.mjs +707 -121
- package/bin/ark-mcp-runtime.mjs +448 -169
- package/bin/ark-mcp.mjs +68 -4
- package/bin/ark-shared.mjs +42 -22
- package/bin/ark.mjs +65 -97
- package/bin/lib/adapter-contract-types.mjs +5 -0
- package/bin/lib/adapter-contract.mjs +15 -0
- 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 +9 -5
- package/bin/lib/ambient-state.mjs +9 -30
- package/bin/lib/analysis-engine.mjs +15 -11
- package/bin/lib/architecture-scan.mjs +61 -15
- package/bin/lib/ark-order-doctor.mjs +4 -29
- package/bin/lib/ark-order-facts.mjs +319 -17
- package/bin/lib/ark-order-sensors.mjs +28 -101
- package/bin/lib/ark-order-types.mjs +3 -2
- package/bin/lib/ark-run-doctor.mjs +17 -28
- package/bin/lib/ark-run-facts.mjs +461 -48
- package/bin/lib/ark-run-sensors.mjs +6 -4
- package/bin/lib/arkgate-pins.mjs +91 -0
- package/bin/lib/arkrule-file-hints.mjs +2 -47
- package/bin/lib/arkrules-contract.mjs +124 -133
- package/bin/lib/arkrules-sensors.mjs +55 -252
- package/bin/lib/check-args.mjs +85 -1
- package/bin/lib/ci-and-commands.mjs +24 -78
- package/bin/lib/ci-merge-boundary.mjs +1 -1
- package/bin/lib/class-source-scan.mjs +479 -0
- package/bin/lib/clone-detection.mjs +474 -0
- package/bin/lib/codex-home.mjs +5 -3
- package/bin/lib/config-contract-slices.mjs +739 -0
- package/bin/lib/config-contract.mjs +42 -607
- package/bin/lib/config-extras.mjs +16 -22
- package/bin/lib/config-warnings.mjs +32 -0
- package/bin/lib/contract-smells.mjs +33 -15
- package/bin/lib/core-ratchet.mjs +0 -2
- package/bin/lib/dashboard-cli.mjs +62 -0
- package/bin/lib/deep-module-coach.mjs +2 -4
- package/bin/lib/deploy-path.mjs +13 -38
- package/bin/lib/design-delta.mjs +1 -1
- package/bin/lib/design-smells.mjs +2 -2
- package/bin/lib/diagnostic-catalog.mjs +23 -9
- package/bin/lib/doctor-advisories.mjs +78 -8
- package/bin/lib/doctor-green-cite.mjs +1 -1
- package/bin/lib/doctor-human.mjs +1 -1
- package/bin/lib/doctor-next-actions.mjs +19 -4
- package/bin/lib/doctor-plan.mjs +9 -13
- package/bin/lib/duplication-io.mjs +572 -0
- package/bin/lib/effective-contract-load.mjs +282 -83
- package/bin/lib/enforcement-profiles.mjs +40 -4
- 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 +10 -5
- package/bin/lib/flat-parent-importers.mjs +139 -0
- package/bin/lib/flat-parent-pilot.mjs +40 -100
- package/bin/lib/gate-files.mjs +58 -90
- package/bin/lib/git-change-scope.mjs +109 -0
- package/bin/lib/github-enforcement.mjs +166 -29
- package/bin/lib/golden-pattern.mjs +1 -1
- package/bin/lib/hook-templates.mjs +17 -15
- package/bin/lib/host-support-matrix.mjs +3 -1
- 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/import-resolve.mjs +13 -1
- package/bin/lib/improvement-compass-doctor.mjs +1 -24
- package/bin/lib/install-migrate.mjs +50 -73
- package/bin/lib/invariant-coverage-io.mjs +4 -3
- package/bin/lib/invariant-coverage.mjs +85 -11
- 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 +13 -6
- package/bin/lib/mcp-hook-payload.mjs +403 -63
- 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 +60 -2
- package/bin/lib/parse-health.mjs +1 -1
- package/bin/lib/passthrough-commands.mjs +83 -0
- package/bin/lib/persistence-write-hint.mjs +106 -0
- package/bin/lib/pin-dependent-gates.mjs +117 -0
- package/bin/lib/policy-delta-io.mjs +219 -14
- package/bin/lib/post-green-path.mjs +1 -1
- package/bin/lib/prepare-change.mjs +115 -7
- package/bin/lib/prepare-write.mjs +2 -2
- package/bin/lib/presets.mjs +59 -73
- package/bin/lib/product-copy.mjs +1 -6
- package/bin/lib/project-root.mjs +31 -62
- package/bin/lib/remediation.mjs +23 -4
- package/bin/lib/resolved-candidate-facts.mjs +405 -117
- package/bin/lib/rules-inventory.mjs +22 -3
- package/bin/lib/rules-under-contract.mjs +247 -9
- package/bin/lib/scan-files.mjs +36 -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 +140 -6
- package/bin/lib/source-policy.mjs +34 -0
- package/bin/lib/start-install-recovery.mjs +72 -2
- package/bin/lib/start-preview.mjs +31 -1
- package/bin/lib/states-transitions-presence.mjs +3 -3
- package/bin/lib/status-command.mjs +35 -22
- package/bin/lib/status-manifest.mjs +50 -30
- package/bin/lib/status-transition-catalog.mjs +0 -6
- package/bin/lib/suggestions.mjs +3 -3
- package/bin/lib/team-parliament-io.mjs +76 -88
- package/bin/lib/team-parliament.mjs +21 -10
- 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 +6 -4
- package/dist/chunk-A5EIY3HI.cjs +5 -0
- package/dist/chunk-AHACEKSV.js +16 -0
- package/dist/chunk-SVSQVGFJ.js +5 -0
- package/dist/chunk-UN5EH4QV.cjs +16 -0
- package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-GPvvdleM.d.cts} +30 -4
- package/dist/configTypes-GPvvdleM.d.ts +257 -0
- package/dist/eslint/index.cjs +18 -5
- package/dist/eslint/index.d.cts +368 -0
- package/dist/eslint/index.d.ts +139 -156
- package/dist/eslint/index.js +11 -5
- package/dist/index.cjs +36 -35
- package/dist/index.d.cts +5547 -0
- package/dist/index.d.ts +3283 -198
- package/dist/index.js +36 -35
- package/dist/inspectorListen-6DQVJ4DC.js +1 -0
- package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
- package/dist/nestjs/index.cjs +1 -18
- package/dist/nestjs/index.d.cts +22 -0
- package/dist/nestjs/index.d.ts +1 -3
- package/dist/nestjs/index.js +1 -18
- package/dist/order/index.cjs +1 -1
- package/dist/order/index.d.cts +155 -0
- package/dist/order/index.d.ts +15 -3
- package/dist/order/index.js +1 -1
- package/dist/runtime/index.cjs +13 -30
- package/dist/{diagnosticCatalog-35mrwAHe.d.ts → runtime/index.d.cts} +828 -276
- package/dist/runtime/index.d.ts +2741 -133
- package/dist/runtime/index.js +13 -30
- package/dist/{types-Bdj32QwA.d.ts → types-CnauMQQL.d.cts} +607 -163
- package/dist/types-CnauMQQL.d.ts +1800 -0
- package/docs/README.md +3 -1
- package/docs/agent-guide.md +217 -16
- package/docs/ai-gates.md +174 -26
- package/docs/arkorder.md +32 -15
- package/docs/brownfield-adoption.md +5 -1
- package/docs/configuration.md +230 -31
- package/docs/develop.md +2 -1
- package/docs/diagnostics.md +212 -19
- package/docs/package-surface.md +35 -9
- package/docs/product-voice.md +7 -1
- package/package.json +69 -19
- package/schemas/ark.analysis-result.schema.json +15 -0
- package/schemas/ark.config.schema.json +77 -12
- package/schemas/ark.invariant-probe.schema.json +368 -0
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
- package/schemas/ark.status-manifest.schema.json +22 -0
- package/server.json +3 -9
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +5 -1
- 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-order/SKILL.md +4 -2
- package/templates/agent-skills/ark-place/SKILL.md +8 -0
- package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
- package/templates/arkrules/PersistenceAdapters.json +1 -1
- package/templates/arkrules/PresentationAdapters.json +1 -1
- package/templates/skills/ark-adopt.md +5 -1
- 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-order.md +4 -2
- package/templates/skills/ark-place.md +8 -0
- package/templates/skills/ark-runtime.md +6 -1
- package/bin/lib/ark-order-error.mjs +0 -18
- package/bin/lib/ark-order-invariants.mjs +0 -346
- package/dist/types-Ik_OakcI.d.ts +0 -183
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { DOMAIN_EVENTS_PUSH_RE, DOMAIN_INVARIANT_WORD_RE, expectedDomainInvariantWordsPhrase, isIdiomaticEventsReset, } from './arkrules-sensors.mjs';
|
|
12
|
+
/** Baseline key belongs to the ArkRules plane (structure sensors or invariants). */
|
|
13
|
+
export function isArkRulesFrozenKey(key) {
|
|
14
|
+
const ruleId = key.split('|', 1)[0] ?? '';
|
|
15
|
+
return ruleId.startsWith('ARKRULE_') || ruleId.startsWith('INVARIANT_');
|
|
16
|
+
}
|
|
12
17
|
function lineOf(content, index) {
|
|
13
18
|
return content.slice(0, index).split('\n').length;
|
|
14
19
|
}
|
|
@@ -274,13 +279,27 @@ export function buildRulesInventory(input) {
|
|
|
274
279
|
a.line - b.line ||
|
|
275
280
|
a.kind.localeCompare(b.kind) ||
|
|
276
281
|
a.id.localeCompare(b.id));
|
|
277
|
-
const
|
|
278
|
-
|
|
282
|
+
const contractedSensors = new Set((input.contractedStructure ?? []).map((rule) => `${rule.layer}\u0000${rule.sensor}`));
|
|
283
|
+
const underContract = candidates.filter((c) => {
|
|
284
|
+
const suggestion = c.suggestedArkRule;
|
|
285
|
+
if (!suggestion)
|
|
286
|
+
return false;
|
|
287
|
+
if (suggestion.invariantId && contracted.has(suggestion.invariantId))
|
|
288
|
+
return true;
|
|
289
|
+
if (!suggestion.structureId)
|
|
290
|
+
return false;
|
|
291
|
+
// Structure suggestions: the sensor on that layer is what enforces, not the id string.
|
|
292
|
+
if (suggestion.sensor &&
|
|
293
|
+
contractedSensors.has(`${suggestion.layer}\u0000${suggestion.sensor}`)) {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
return contracted.has(suggestion.structureId);
|
|
297
|
+
}).length;
|
|
279
298
|
return {
|
|
280
299
|
candidates,
|
|
281
300
|
inventoried: candidates.length,
|
|
282
301
|
underContract,
|
|
283
|
-
frozen: (input.frozenKeys ?? []).length,
|
|
302
|
+
frozen: (input.frozenKeys ?? []).filter(isArkRulesFrozenKey).length,
|
|
284
303
|
notAScore: true,
|
|
285
304
|
};
|
|
286
305
|
}
|
|
@@ -4,21 +4,25 @@
|
|
|
4
4
|
* Summary includes per-layer + structure/invariant detail so showcase HTML /ark-explain
|
|
5
5
|
* can teach what is under contract, not only aggregate numbers.
|
|
6
6
|
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
7
8
|
import path from 'node:path';
|
|
8
9
|
import { loadEffectiveArkRulesFromDisk } from './effective-contract-load.mjs';
|
|
10
|
+
import { buildRulesInventory, inventoryToExtractionCard } from './rules-inventory.mjs';
|
|
11
|
+
import { collectGovernedFiles } from './scan-files.mjs';
|
|
12
|
+
import { readBaseline } from './violations.mjs';
|
|
9
13
|
import { evaluateInvariantCoverage, formatCoverageDiscards } from './invariant-coverage.mjs';
|
|
10
14
|
import {
|
|
11
15
|
coverageOptionsFromConfig,
|
|
12
16
|
invariantIdsFromCatalog,
|
|
13
17
|
loadInvariantCoverageInputs,
|
|
14
18
|
} from './invariant-coverage-io.mjs';
|
|
19
|
+
import { formatProbeStatusLines, probeStatusSection } from './invariant-probe-io.mjs';
|
|
15
20
|
import {
|
|
16
|
-
EXTRA_MERGE_TEETH_GOVERNED_FLOOR,
|
|
17
21
|
composeMergePlanesHonesty,
|
|
18
22
|
demoteExtraPlaneTeethUnderClassificationFloor,
|
|
19
23
|
} from './extra-merge-teeth.mjs';
|
|
20
24
|
import { collectEmptyInvariantCatalogFindings } from './arkrules-sensors.mjs';
|
|
21
|
-
import { layerForRelativePath } from '../ark-layer-match.mjs';
|
|
25
|
+
import { layerForFile, layerForRelativePath } from '../ark-layer-match.mjs';
|
|
22
26
|
import {
|
|
23
27
|
ARKRULES_EMPTY_CATALOG_NEXT,
|
|
24
28
|
ARKRULES_FIRST_CONTACT_NEXT,
|
|
@@ -35,8 +39,6 @@ const COVERED_SAMPLE_MAX = 24;
|
|
|
35
39
|
const STRUCTURE_CATALOG_MAX = 40;
|
|
36
40
|
const UNCOVERED_CATALOG_MAX = 30;
|
|
37
41
|
|
|
38
|
-
export { EXTRA_MERGE_TEETH_GOVERNED_FLOOR };
|
|
39
|
-
|
|
40
42
|
/** Doctor walk often hands absolute paths; layer globs are project-relative. */
|
|
41
43
|
function projectRelativePath(root, filePath) {
|
|
42
44
|
const posix = String(filePath).replace(/\\/g, '/');
|
|
@@ -96,9 +98,80 @@ function arkOrderMergeInput(config, residualCount = 0) {
|
|
|
96
98
|
};
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
|
|
101
|
+
function rulesBySlice(arkRules) {
|
|
102
|
+
const groups = new Map();
|
|
103
|
+
const rows = [...(arkRules?.structure ?? []), ...(arkRules?.invariants ?? [])];
|
|
104
|
+
for (const entry of rows) {
|
|
105
|
+
const childId = entry?.provenance?.childId;
|
|
106
|
+
const sourceFile = entry?.provenance?.sourceFile;
|
|
107
|
+
if (typeof childId !== 'string' || typeof sourceFile !== 'string') continue;
|
|
108
|
+
const key = `${childId}\0${sourceFile}`;
|
|
109
|
+
let group = groups.get(key);
|
|
110
|
+
if (!group) {
|
|
111
|
+
group = {
|
|
112
|
+
slice: childId,
|
|
113
|
+
layer: entry.provenance?.layer ?? null,
|
|
114
|
+
sourceFile,
|
|
115
|
+
ids: [],
|
|
116
|
+
appliesTo: [],
|
|
117
|
+
};
|
|
118
|
+
groups.set(key, group);
|
|
119
|
+
}
|
|
120
|
+
if (typeof entry.id === 'string' && !group.ids.includes(entry.id)) group.ids.push(entry.id);
|
|
121
|
+
for (const pattern of entry.appliesTo ?? []) {
|
|
122
|
+
if (typeof pattern === 'string' && !group.appliesTo.includes(pattern)) group.appliesTo.push(pattern);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return [...groups.values()]
|
|
126
|
+
.map((group) => ({
|
|
127
|
+
...group,
|
|
128
|
+
ids: [...group.ids].sort(),
|
|
129
|
+
appliesTo: [...group.appliesTo].sort(),
|
|
130
|
+
}))
|
|
131
|
+
.sort((left, right) => left.slice.localeCompare(right.slice) || left.sourceFile.localeCompare(right.sourceFile));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** ADR 0012 D2 drift: arkrules/*.json no arkRules entry references (advisory). */
|
|
135
|
+
function unreferencedArkRulesFiles(loaded) {
|
|
136
|
+
const files = (loaded?.warnings ?? []).map((w) => w.path).filter(Boolean);
|
|
137
|
+
return files.length > 0 ? { unreferencedFiles: files } : {};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {{ rulesMigration?: boolean }} [options] rulesMigration:false skips the
|
|
142
|
+
* migration counts (the inventory payload computes them itself).
|
|
143
|
+
*/
|
|
144
|
+
function governedPathsFromFacts(root, facts) {
|
|
145
|
+
const rows = facts?.files;
|
|
146
|
+
if (!Array.isArray(rows) || rows.length === 0) return undefined;
|
|
147
|
+
const base = String(root).replace(/\\/g, '/').replace(/\/+$/, '');
|
|
148
|
+
const prefix = `${base}/`;
|
|
149
|
+
const out = [];
|
|
150
|
+
for (const entry of rows) {
|
|
151
|
+
const raw = typeof entry === 'string' ? entry : entry?.path;
|
|
152
|
+
if (typeof raw !== 'string' || raw.length === 0) continue;
|
|
153
|
+
const norm = raw.replace(/\\/g, '/');
|
|
154
|
+
const rel = norm.startsWith(prefix)
|
|
155
|
+
? norm.slice(prefix.length)
|
|
156
|
+
: norm.replace(/^\.\//, '');
|
|
157
|
+
if (!rel || rel.startsWith('..') || rel.startsWith('/')) continue;
|
|
158
|
+
out.push(rel);
|
|
159
|
+
}
|
|
160
|
+
return out.length > 0 ? out : undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function summarizeRulesUnderContract(root, config, facts, classification, options = {}) {
|
|
164
|
+
const governedFiles = governedPathsFromFacts(root, facts);
|
|
165
|
+
const loadOpts = governedFiles ? { files: governedFiles } : {};
|
|
100
166
|
if (!config?.arkRules || Object.keys(config.arkRules).length === 0) {
|
|
167
|
+
let drift = {};
|
|
168
|
+
try {
|
|
169
|
+
drift = unreferencedArkRulesFiles(loadEffectiveArkRulesFromDisk(root, config, loadOpts));
|
|
170
|
+
} catch {
|
|
171
|
+
drift = {};
|
|
172
|
+
}
|
|
101
173
|
return {
|
|
174
|
+
...drift,
|
|
102
175
|
active: false,
|
|
103
176
|
structureRules: 0,
|
|
104
177
|
invariants: 0,
|
|
@@ -115,7 +188,7 @@ export function summarizeRulesUnderContract(root, config, facts, classification)
|
|
|
115
188
|
};
|
|
116
189
|
}
|
|
117
190
|
try {
|
|
118
|
-
const loaded = loadEffectiveArkRulesFromDisk(root, config);
|
|
191
|
+
const loaded = loadEffectiveArkRulesFromDisk(root, config, loadOpts);
|
|
119
192
|
if (loaded.errors?.length) {
|
|
120
193
|
return {
|
|
121
194
|
active: true,
|
|
@@ -146,6 +219,7 @@ export function summarizeRulesUnderContract(root, config, facts, classification)
|
|
|
146
219
|
const covById = new Map(
|
|
147
220
|
(coverage.coverage ?? []).map((row) => [row.invariantId, row])
|
|
148
221
|
);
|
|
222
|
+
const probeSection = probeStatusSection(root, loaded.arkRules.invariants);
|
|
149
223
|
const byLayer = loaded.arkRules.byLayer ?? {};
|
|
150
224
|
const layers = Object.keys(byLayer)
|
|
151
225
|
.sort((a, b) => a.localeCompare(b))
|
|
@@ -159,6 +233,7 @@ export function summarizeRulesUnderContract(root, config, facts, classification)
|
|
|
159
233
|
return {
|
|
160
234
|
name,
|
|
161
235
|
sourceFile: part.sourceFile ?? null,
|
|
236
|
+
...(Array.isArray(part.sourceFiles) ? { sourceFiles: [...part.sourceFiles] } : {}),
|
|
162
237
|
structureRules: (part.structure ?? []).length,
|
|
163
238
|
invariants: layerInvariants.length,
|
|
164
239
|
coveredInvariants: covered,
|
|
@@ -191,7 +266,7 @@ export function summarizeRulesUnderContract(root, config, facts, classification)
|
|
|
191
266
|
|
|
192
267
|
const symbolEvidence = (coverage.coverage ?? [])
|
|
193
268
|
.filter((row) => typeof row.symbolEvidenceFile === 'string' && row.symbolEvidenceFile.length > 0)
|
|
194
|
-
.map((row) => ({ id: row.invariantId, file: row.symbolEvidenceFile }));
|
|
269
|
+
.map((row) => ({ id: row.invariantId, file: projectRelativePath(root, row.symbolEvidenceFile) }));
|
|
195
270
|
const coveredAll = (coverage.coverage ?? [])
|
|
196
271
|
.filter((row) => row.covered)
|
|
197
272
|
.map((row) => ({
|
|
@@ -200,7 +275,7 @@ export function summarizeRulesUnderContract(root, config, facts, classification)
|
|
|
200
275
|
mode: row.mode ?? null,
|
|
201
276
|
description: row.description ?? null,
|
|
202
277
|
...(typeof row.symbolEvidenceFile === 'string' && row.symbolEvidenceFile.length > 0
|
|
203
|
-
? { symbolEvidenceFile: row.symbolEvidenceFile }
|
|
278
|
+
? { symbolEvidenceFile: projectRelativePath(root, row.symbolEvidenceFile) }
|
|
204
279
|
: {}),
|
|
205
280
|
}));
|
|
206
281
|
const coveredTruncated = Math.max(0, coveredAll.length - COVERED_SAMPLE_MAX);
|
|
@@ -259,6 +334,7 @@ export function summarizeRulesUnderContract(root, config, facts, classification)
|
|
|
259
334
|
partialCoverage: coverage.partial,
|
|
260
335
|
testFilesScanned: coverageInputs.testFiles.length,
|
|
261
336
|
layers,
|
|
337
|
+
...(rulesBySlice(loaded.arkRules).length > 0 ? { bySlice: rulesBySlice(loaded.arkRules) } : {}),
|
|
262
338
|
structure,
|
|
263
339
|
structureTruncated,
|
|
264
340
|
uncovered,
|
|
@@ -266,7 +342,17 @@ export function summarizeRulesUnderContract(root, config, facts, classification)
|
|
|
266
342
|
coveredSample,
|
|
267
343
|
coveredTruncated,
|
|
268
344
|
symbolEvidence,
|
|
345
|
+
...unreferencedArkRulesFiles(loaded),
|
|
346
|
+
// AR15: inventoried / under contract / frozen — same helper as --rules-inventory.
|
|
347
|
+
...(options.rulesMigration === false
|
|
348
|
+
? {}
|
|
349
|
+
: {
|
|
350
|
+
rulesMigration: buildRulesMigration({ root, config, arkRules: loaded.arkRules })
|
|
351
|
+
.rulesMigration,
|
|
352
|
+
}),
|
|
269
353
|
...(coverageInputs.stats ? { coverageStats: coverageInputs.stats } : {}),
|
|
354
|
+
// ADR 0039: only when a probe artifact exists — absence stays silent.
|
|
355
|
+
...(probeSection ? { probe: probeSection } : {}),
|
|
270
356
|
mergePlanes,
|
|
271
357
|
notAScore: true,
|
|
272
358
|
note: 'ArkRules plane (intra-layer) — counts and catalog, never a score. Green with uncovered residual must say so. Structure sensors are heuristics; invariants are catalog+coverage evidence, not a business runtime.',
|
|
@@ -306,6 +392,7 @@ export function formatArkRulesEvidenceLines(section) {
|
|
|
306
392
|
}
|
|
307
393
|
const discard = formatCoverageDiscards(section.coverageStats);
|
|
308
394
|
if (discard) lines.push(`ArkRules:${discard}`);
|
|
395
|
+
lines.push(...formatProbeStatusLines(section.probe));
|
|
309
396
|
return lines;
|
|
310
397
|
}
|
|
311
398
|
|
|
@@ -314,9 +401,19 @@ export function formatArkRulesEvidenceLines(section) {
|
|
|
314
401
|
* @param {ReturnType<typeof summarizeRulesUnderContract>|null|undefined} section
|
|
315
402
|
* @returns {string[]}
|
|
316
403
|
*/
|
|
404
|
+
function unreferencedFilesLine(section) {
|
|
405
|
+
const files = Array.isArray(section?.unreferencedFiles) ? section.unreferencedFiles : [];
|
|
406
|
+
if (files.length === 0) return [];
|
|
407
|
+
const shown = files.slice(0, 3).join(', ');
|
|
408
|
+
const more = files.length > 3 ? ` (+${files.length - 3} more)` : '';
|
|
409
|
+
return [
|
|
410
|
+
`ArkRules: ${shown}${more} not referenced by arkRules — nothing in it is enforced (ARKRULE_FILE_UNREFERENCED, advisory).`,
|
|
411
|
+
];
|
|
412
|
+
}
|
|
413
|
+
|
|
317
414
|
export function formatArkRulesDoctorLines(section) {
|
|
318
415
|
if (!section || typeof section !== 'object') return [];
|
|
319
|
-
if (section.active !== true) return
|
|
416
|
+
if (section.active !== true) return unreferencedFilesLine(section);
|
|
320
417
|
|
|
321
418
|
if (Array.isArray(section.loadErrors) && section.loadErrors.length > 0) {
|
|
322
419
|
const first = section.loadErrors[0];
|
|
@@ -362,6 +459,10 @@ export function formatArkRulesDoctorLines(section) {
|
|
|
362
459
|
lines.push(`ArkRules: ${section.note}`);
|
|
363
460
|
}
|
|
364
461
|
lines.push(...formatArkRulesEvidenceLines(section));
|
|
462
|
+
if (section.rulesMigration) {
|
|
463
|
+
lines.push(`ArkRules migration: ${formatRulesMigrationCounts(section.rulesMigration)}.`);
|
|
464
|
+
}
|
|
465
|
+
lines.push(...unreferencedFilesLine(section));
|
|
365
466
|
return lines;
|
|
366
467
|
}
|
|
367
468
|
|
|
@@ -559,6 +660,11 @@ export function formatRulesUnderContractHtml(section, esc) {
|
|
|
559
660
|
}
|
|
560
661
|
${uncoveredBlock}
|
|
561
662
|
${coveredBlock}
|
|
663
|
+
${
|
|
664
|
+
section.rulesMigration
|
|
665
|
+
? `<p class="muted" style="margin-top:.35rem;font-size:.84rem">Rules migration: ${escape(formatRulesMigrationCounts(section.rulesMigration))}</p>`
|
|
666
|
+
: ''
|
|
667
|
+
}
|
|
562
668
|
${
|
|
563
669
|
formatArkRulesEvidenceLines(section)
|
|
564
670
|
.map((line) => `<p class="muted" style="margin-top:.35rem;font-size:.84rem">${escape(line)}</p>`)
|
|
@@ -572,3 +678,135 @@ export function formatRulesUnderContractHtml(section, esc) {
|
|
|
572
678
|
${note}
|
|
573
679
|
</section>`;
|
|
574
680
|
}
|
|
681
|
+
|
|
682
|
+
const RULES_INVENTORY_MAX_FILES = 400;
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Brownfield rules migration (AR13/AR15): one code path for `--rules-inventory`,
|
|
686
|
+
* MCP `ark_rules_inventory`, doctor, and the HTML report. Honest counts, never a score:
|
|
687
|
+
* - inventoried: candidates the heuristic inventory found (first 400 governed files);
|
|
688
|
+
* - underContract: candidates whose suggested ArkRule is declared — structure
|
|
689
|
+
* suggestions by (layer, sensor), invariant suggestions by id;
|
|
690
|
+
* - frozen: ArkRules-plane keys (`ARKRULE_*` / `INVARIANT_*`) in .ark-baseline.json.
|
|
691
|
+
* @param {{ root: string, config: Record<string, any>, files?: string[], arkRules?: any }} input
|
|
692
|
+
* `files` defaults to the governed walk; `arkRules` to the catalog on disk.
|
|
693
|
+
*/
|
|
694
|
+
function buildRulesMigration({ root, config, files, arkRules }) {
|
|
695
|
+
const governed = Array.isArray(files) ? files : collectGovernedFiles(root, config);
|
|
696
|
+
const fileContents = {};
|
|
697
|
+
const fileLayers = {};
|
|
698
|
+
for (const file of governed.slice(0, RULES_INVENTORY_MAX_FILES)) {
|
|
699
|
+
const absolute = path.isAbsolute(file) ? file : path.resolve(root, file);
|
|
700
|
+
const rel = path.relative(root, absolute).split(path.sep).join('/');
|
|
701
|
+
try {
|
|
702
|
+
fileContents[rel] = fs.readFileSync(absolute, 'utf8');
|
|
703
|
+
const layer = layerForFile(root, absolute, config.layers);
|
|
704
|
+
if (layer) fileLayers[rel] = layer;
|
|
705
|
+
} catch {
|
|
706
|
+
/* skip unreadable */
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
let catalog = arkRules;
|
|
710
|
+
if (catalog === undefined && config?.arkRules) {
|
|
711
|
+
try {
|
|
712
|
+
const loaded = loadEffectiveArkRulesFromDisk(root, config, {
|
|
713
|
+
files: governed.map((file) => {
|
|
714
|
+
const absolute = path.isAbsolute(file) ? file : path.resolve(root, file);
|
|
715
|
+
return path.relative(root, absolute).split(path.sep).join('/');
|
|
716
|
+
}),
|
|
717
|
+
});
|
|
718
|
+
catalog = loaded.errors.length > 0 ? null : loaded.arkRules;
|
|
719
|
+
} catch {
|
|
720
|
+
catalog = null; // the advisory inventory is still useful
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const contractedRuleIds = [];
|
|
724
|
+
const contractedStructure = [];
|
|
725
|
+
for (const rule of catalog?.structure ?? []) {
|
|
726
|
+
contractedRuleIds.push(rule.id);
|
|
727
|
+
const layer = rule.provenance?.layer ?? rule.layer;
|
|
728
|
+
if (layer && rule.sensor) contractedStructure.push({ layer, sensor: rule.sensor });
|
|
729
|
+
}
|
|
730
|
+
for (const inv of catalog?.invariants ?? []) contractedRuleIds.push(inv.id);
|
|
731
|
+
let frozenKeys = [];
|
|
732
|
+
try {
|
|
733
|
+
frozenKeys = [...readBaseline(root, '.ark-baseline.json').keys];
|
|
734
|
+
} catch {
|
|
735
|
+
frozenKeys = []; // an unreadable baseline freezes nothing
|
|
736
|
+
}
|
|
737
|
+
const inventory = buildRulesInventory({
|
|
738
|
+
fileContents,
|
|
739
|
+
fileLayers,
|
|
740
|
+
layerContexts: (config.layers ?? []).map((layer) => ({
|
|
741
|
+
name: layer.name,
|
|
742
|
+
intentPrefixes: layer.intentPrefixes ?? [],
|
|
743
|
+
})),
|
|
744
|
+
contractedRuleIds,
|
|
745
|
+
contractedStructure,
|
|
746
|
+
frozenKeys,
|
|
747
|
+
});
|
|
748
|
+
return {
|
|
749
|
+
inventory,
|
|
750
|
+
rulesMigration: {
|
|
751
|
+
inventoried: inventory.inventoried,
|
|
752
|
+
underContract: inventory.underContract,
|
|
753
|
+
frozen: inventory.frozen,
|
|
754
|
+
notAScore: true,
|
|
755
|
+
},
|
|
756
|
+
nextPilot:
|
|
757
|
+
inventory.candidates[0] != null ? inventoryToExtractionCard(inventory.candidates[0]) : null,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/** `N inventoried, N under contract, N frozen (not a score)` — shared CLI/doctor wording. */
|
|
762
|
+
export function formatRulesMigrationCounts(rulesMigration) {
|
|
763
|
+
if (!rulesMigration || typeof rulesMigration !== 'object') return '';
|
|
764
|
+
return `${rulesMigration.inventoried} inventoried, ${rulesMigration.underContract} under contract, ${rulesMigration.frozen} frozen (not a score)`;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Full `--rules-inventory` / `ark_rules_inventory` payload (same keys on both surfaces).
|
|
769
|
+
* @param {string} root
|
|
770
|
+
* @param {Record<string, any>} config
|
|
771
|
+
* @param {string[]} [files] governed files (absolute or root-relative)
|
|
772
|
+
*/
|
|
773
|
+
export function buildRulesInventoryPayload(root, config, files) {
|
|
774
|
+
const governed = Array.isArray(files) ? files : collectGovernedFiles(root, config);
|
|
775
|
+
const migration = buildRulesMigration({ root, config, files: governed });
|
|
776
|
+
let evidenceLines = [];
|
|
777
|
+
let coverageEvidence = null;
|
|
778
|
+
if (config?.arkRules && Object.keys(config.arkRules).length > 0) {
|
|
779
|
+
try {
|
|
780
|
+
const section = summarizeRulesUnderContract(
|
|
781
|
+
root,
|
|
782
|
+
config,
|
|
783
|
+
{
|
|
784
|
+
files: governed.map((file) => ({
|
|
785
|
+
path: path.relative(root, path.resolve(root, file)).split(path.sep).join('/'),
|
|
786
|
+
})),
|
|
787
|
+
},
|
|
788
|
+
undefined,
|
|
789
|
+
{ rulesMigration: false }
|
|
790
|
+
);
|
|
791
|
+
evidenceLines = formatArkRulesEvidenceLines(section);
|
|
792
|
+
if (section.active === true) {
|
|
793
|
+
coverageEvidence = {
|
|
794
|
+
symbolEvidence: section.symbolEvidence ?? [],
|
|
795
|
+
discarded: section.coverageStats?.discarded ?? null,
|
|
796
|
+
...(section.probe ? { probe: section.probe } : {}),
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
} catch {
|
|
800
|
+
/* inventory still useful when coverage cannot be read */
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return {
|
|
804
|
+
payload: {
|
|
805
|
+
rulesInventory: migration.inventory,
|
|
806
|
+
rulesMigration: migration.rulesMigration,
|
|
807
|
+
nextPilot: migration.nextPilot,
|
|
808
|
+
...(coverageEvidence ? { coverageEvidence } : {}),
|
|
809
|
+
},
|
|
810
|
+
evidenceLines,
|
|
811
|
+
};
|
|
812
|
+
}
|
package/bin/lib/scan-files.mjs
CHANGED
|
@@ -8,12 +8,12 @@ import { isScanExcludedRelative, isSkippedSourceDir } from '../ark-shared.mjs';
|
|
|
8
8
|
|
|
9
9
|
export { isSkippedSourceDir };
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const SOURCE_FILE_NAME = /\.[cm]?[tj]sx?$/;
|
|
12
12
|
|
|
13
13
|
/** Unit/e2e test files are not architecture surface — agents and Nest put them next
|
|
14
14
|
* to production code (*.spec.ts). Counting them as ungoverned forces false
|
|
15
15
|
* CONFIG_UNCLASSIFIED_FILES under --strict-config on every starter. */
|
|
16
|
-
|
|
16
|
+
const TEST_FILE_NAME =
|
|
17
17
|
/(^test(?:[-_.]).*|\.(spec|test)(?:-d)?\.)(tsx?|jsx?|mjsx?|cjsx?|mts|cts)$/i;
|
|
18
18
|
|
|
19
19
|
export function isGovernableSourceFile(name) {
|
|
@@ -32,6 +32,17 @@ function isInsideRoot(root, target) {
|
|
|
32
32
|
* Internal symlink directories are followed once (TypeScript follows them too),
|
|
33
33
|
* while escaping links fail closed instead of reading arbitrary filesystem paths.
|
|
34
34
|
*/
|
|
35
|
+
/**
|
|
36
|
+
* Return `value` flattened. V8 keeps path.join / concatenation results as rope
|
|
37
|
+
* strings (a tree of segment objects); the per-file syscalls of the generic walk
|
|
38
|
+
* branch flattened them as a side effect. Without it, every retained path kept its
|
|
39
|
+
* rope alive and a 20k-file walk peaked ~70 MB higher. ToNumber flattens in place.
|
|
40
|
+
*/
|
|
41
|
+
function flatString(value) {
|
|
42
|
+
void Number(value);
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
35
46
|
export function walk(dir, files = [], options = {}) {
|
|
36
47
|
let state = options.state;
|
|
37
48
|
if (!state) {
|
|
@@ -67,6 +78,7 @@ export function walk(dir, files = [], options = {}) {
|
|
|
67
78
|
!state.visitedFiles.has(resolved)
|
|
68
79
|
) {
|
|
69
80
|
state.visitedFiles.add(resolved);
|
|
81
|
+
state.onFile?.(dir, resolved);
|
|
70
82
|
files.push(dir);
|
|
71
83
|
}
|
|
72
84
|
return files;
|
|
@@ -84,6 +96,25 @@ export function walk(dir, files = [], options = {}) {
|
|
|
84
96
|
} else if (entry.isSymbolicLink()) {
|
|
85
97
|
if (isSkippedSourceDir(entry.name)) continue;
|
|
86
98
|
walk(full, files, { state });
|
|
99
|
+
} else if (entry.isFile()) {
|
|
100
|
+
// A regular (non-link) file inside an already-resolved directory: its real
|
|
101
|
+
// path is that directory's real path plus its name. Same result as the
|
|
102
|
+
// per-file lstat + realpath of the generic branch, without two syscalls
|
|
103
|
+
// and two Stats objects for every source file.
|
|
104
|
+
if (!isGovernableSourceFile(entry.name)) continue;
|
|
105
|
+
state.observeInput?.(path.resolve(full), 'lstat');
|
|
106
|
+
state.observeInput?.(path.resolve(full), 'realpath');
|
|
107
|
+
// `resolved` is already a normalized real path and an entry name has no
|
|
108
|
+
// separator, so plain concatenation equals path.join(resolved, name).
|
|
109
|
+
const real = flatString(
|
|
110
|
+
resolved.endsWith(path.sep)
|
|
111
|
+
? `${resolved}${entry.name}`
|
|
112
|
+
: `${resolved}${path.sep}${entry.name}`
|
|
113
|
+
);
|
|
114
|
+
if (state.visitedFiles.has(real)) continue;
|
|
115
|
+
state.visitedFiles.add(real);
|
|
116
|
+
state.onFile?.(full, real);
|
|
117
|
+
files.push(flatString(full));
|
|
87
118
|
} else if (isGovernableSourceFile(entry.name)) {
|
|
88
119
|
walk(full, files, { state });
|
|
89
120
|
}
|
|
@@ -92,7 +123,7 @@ export function walk(dir, files = [], options = {}) {
|
|
|
92
123
|
}
|
|
93
124
|
|
|
94
125
|
/** Walk include roots then drop codegen / config.exclude (universal scan filter). */
|
|
95
|
-
|
|
126
|
+
function isIncludeMatch(relativePath, include) {
|
|
96
127
|
return (include ?? []).some((entry) => {
|
|
97
128
|
const includeRoot = String(entry)
|
|
98
129
|
.replace(/\\/g, '/')
|
|
@@ -138,6 +169,7 @@ export function collectGovernedFiles(root, config, options = {}) {
|
|
|
138
169
|
visitedDirectories: new Set(),
|
|
139
170
|
visitedFiles: new Set(),
|
|
140
171
|
onDirectory: options.onDirectory,
|
|
172
|
+
onFile: options.onFile,
|
|
141
173
|
observeInput: options.observeInput,
|
|
142
174
|
};
|
|
143
175
|
const raw = (config.include ?? []).flatMap((entry) =>
|
|
@@ -160,7 +192,7 @@ export function normalize(value) {
|
|
|
160
192
|
export const UNGOVERNED_PROBE_CAP = 200;
|
|
161
193
|
|
|
162
194
|
/** Tooling configs (vite.config.ts, eslint.config.js …) are not the product source a contract governs. */
|
|
163
|
-
|
|
195
|
+
const TOOLING_CONFIG_FILE_NAME = /\.config\.[cm]?[jt]sx?$/i;
|
|
164
196
|
|
|
165
197
|
/**
|
|
166
198
|
* Count governable source files under `root` that the contract's own scope cannot hide.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE — do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* Canonical algorithm: src/domain/schemaValidation.ts
|
|
5
|
+
* Regenerate: node scripts/generate-cli-pure.mjs
|
|
6
|
+
* Drift check: node scripts/generate-cli-pure.mjs --check
|
|
7
|
+
*
|
|
8
|
+
* Pure CLI helper (bin/lib/schema-validation.mjs). Zero Node I/O.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export function isObject(value) {
|
|
12
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
export function propertyPath(parent, key) {
|
|
15
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
|
|
16
|
+
? `${parent}.${key}`
|
|
17
|
+
: `${parent}[${JSON.stringify(key)}]`;
|
|
18
|
+
}
|
|
19
|
+
export function valueType(value) {
|
|
20
|
+
if (value === null)
|
|
21
|
+
return 'null';
|
|
22
|
+
if (Array.isArray(value))
|
|
23
|
+
return 'array';
|
|
24
|
+
return typeof value;
|
|
25
|
+
}
|
|
26
|
+
function resolveSchemaRef(ref, root) {
|
|
27
|
+
const prefix = '#/$defs/';
|
|
28
|
+
if (!ref.startsWith(prefix))
|
|
29
|
+
return undefined;
|
|
30
|
+
return root.$defs[ref.slice(prefix.length)];
|
|
31
|
+
}
|
|
32
|
+
/** Walk `value` against `schema`, appending every finding to `issues`. */
|
|
33
|
+
export function validateSchemaNode(value, schema, path, root, issues) {
|
|
34
|
+
if (schema.$ref) {
|
|
35
|
+
const referenced = resolveSchemaRef(schema.$ref, root);
|
|
36
|
+
if (!referenced) {
|
|
37
|
+
issues.push({ path, message: `schema reference ${schema.$ref} cannot be resolved` });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
validateSchemaNode(value, referenced, path, root, issues);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (schema.const !== undefined && !Object.is(value, schema.const)) {
|
|
44
|
+
issues.push({ path, message: `must equal ${JSON.stringify(schema.const)}` });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (schema.enum && !schema.enum.some((candidate) => Object.is(candidate, value))) {
|
|
48
|
+
issues.push({ path, message: `must be one of ${schema.enum.map(String).join(', ')}` });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (schema.type === 'object') {
|
|
52
|
+
if (!isObject(value)) {
|
|
53
|
+
issues.push({ path, message: `must be an object; received ${valueType(value)}` });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const properties = schema.properties ?? {};
|
|
57
|
+
for (const key of schema.required ?? []) {
|
|
58
|
+
if (value[key] === undefined) {
|
|
59
|
+
issues.push({ path: propertyPath(path, key), message: 'is required' });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (schema.additionalProperties === false) {
|
|
63
|
+
for (const key of Object.keys(value)) {
|
|
64
|
+
if (!(key in properties)) {
|
|
65
|
+
issues.push({ path: propertyPath(path, key), message: 'unknown field' });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (schema.additionalProperties !== undefined &&
|
|
70
|
+
schema.additionalProperties !== true &&
|
|
71
|
+
typeof schema.additionalProperties === 'object') {
|
|
72
|
+
const additional = schema.additionalProperties;
|
|
73
|
+
for (const key of Object.keys(value)) {
|
|
74
|
+
if (!(key in properties)) {
|
|
75
|
+
validateSchemaNode(value[key], additional, propertyPath(path, key), root, issues);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const [key, childSchema] of Object.entries(properties)) {
|
|
80
|
+
if (value[key] !== undefined) {
|
|
81
|
+
validateSchemaNode(value[key], childSchema, propertyPath(path, key), root, issues);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (schema.type === 'array') {
|
|
87
|
+
if (!Array.isArray(value)) {
|
|
88
|
+
issues.push({ path, message: `must be an array; received ${valueType(value)}` });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
92
|
+
issues.push({ path, message: `must contain at least ${schema.minItems} item(s)` });
|
|
93
|
+
}
|
|
94
|
+
if (schema.uniqueItems) {
|
|
95
|
+
const serialized = value.map((entry) => JSON.stringify(entry));
|
|
96
|
+
if (new Set(serialized).size !== serialized.length) {
|
|
97
|
+
issues.push({ path, message: 'must not contain duplicate items' });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (schema.items) {
|
|
101
|
+
value.forEach((entry, index) => validateSchemaNode(entry, schema.items, `${path}[${index}]`, root, issues));
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (schema.type === 'string') {
|
|
106
|
+
if (typeof value !== 'string') {
|
|
107
|
+
issues.push({ path, message: `must be a string; received ${valueType(value)}` });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
111
|
+
issues.push({ path, message: `must contain at least ${schema.minLength} character(s)` });
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (schema.type === 'boolean') {
|
|
116
|
+
if (typeof value !== 'boolean') {
|
|
117
|
+
issues.push({ path, message: `must be a boolean; received ${valueType(value)}` });
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (schema.type === 'integer') {
|
|
122
|
+
if (!Number.isInteger(value)) {
|
|
123
|
+
issues.push({ path, message: `must be an integer; received ${valueType(value)}` });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
127
|
+
issues.push({ path, message: `must be at least ${schema.minimum}` });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -11,18 +11,9 @@ import path from 'node:path';
|
|
|
11
11
|
|
|
12
12
|
import { arkCommand } from '../ark-shared.mjs';
|
|
13
13
|
import { collectGovernedFiles, normalize } from './scan-files.mjs';
|
|
14
|
+
// Same palette and TTY test as the entry's stderr output.
|
|
15
|
+
import { color } from './violations.mjs';
|
|
14
16
|
|
|
15
|
-
// Same shape and the same TTY test as the entry's own `color`: this module is
|
|
16
|
-
// the only other writer to that terminal, and a second copy is cheaper than
|
|
17
|
-
// exporting the entry's internals into a cycle.
|
|
18
|
-
const useColor = process.stderr.isTTY && !process.env.NO_COLOR;
|
|
19
|
-
const color = {
|
|
20
|
-
red: (s) => (useColor ? `\x1b[31m${s}\x1b[0m` : s),
|
|
21
|
-
yellow: (s) => (useColor ? `\x1b[33m${s}\x1b[0m` : s),
|
|
22
|
-
green: (s) => (useColor ? `\x1b[32m${s}\x1b[0m` : s),
|
|
23
|
-
dim: (s) => (useColor ? `\x1b[2m${s}\x1b[0m` : s),
|
|
24
|
-
bold: (s) => (useColor ? `\x1b[1m${s}\x1b[0m` : s),
|
|
25
|
-
};
|
|
26
17
|
|
|
27
18
|
/**
|
|
28
19
|
* A rule id or a source path comes out of the project's own ArkRules JSON and
|