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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tooling I/O for the config version floor (#338): read the files that can pin
|
|
3
|
+
* an arkgate version and hand their text to the pure Domain parser.
|
|
4
|
+
*
|
|
5
|
+
* Bounded and non-recursive. Missing or oversized files are skipped. Every
|
|
6
|
+
* probed path is reported to `observeInput` so a resident snapshot notices a
|
|
7
|
+
* later edit of package.json or a workflow.
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { parseArkgatePins } from './analysis-engine.mjs';
|
|
12
|
+
import { __packageRoot } from './gate-files.mjs';
|
|
13
|
+
|
|
14
|
+
const MAX_PIN_FILE_BYTES = 256 * 1024;
|
|
15
|
+
|
|
16
|
+
const FIXED_CANDIDATES = [
|
|
17
|
+
['package.json', 'package-json'],
|
|
18
|
+
['package-lock.json', 'package-lock'],
|
|
19
|
+
['node_modules/arkgate/package.json', 'installed'],
|
|
20
|
+
['.claude/settings.json', 'hook'],
|
|
21
|
+
['.cursor/hooks.json', 'hook'],
|
|
22
|
+
['.codex/hooks.json', 'hook'],
|
|
23
|
+
['.codex/config.toml', 'hook'],
|
|
24
|
+
['.grok/settings.json', 'hook'],
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/** Directories whose direct children may pin arkgate, with a basename filter. */
|
|
28
|
+
const DIRECTORY_CANDIDATES = [
|
|
29
|
+
['scripts', 'hook', (name) => /hook/i.test(name)],
|
|
30
|
+
['.husky', 'hook', () => true],
|
|
31
|
+
['.githooks', 'hook', () => true],
|
|
32
|
+
['.github/workflows', 'ci', (name) => /\.ya?ml$/i.test(name)],
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function readSmall(absolute) {
|
|
36
|
+
try {
|
|
37
|
+
const stat = fs.statSync(absolute);
|
|
38
|
+
if (!stat.isFile() || stat.size > MAX_PIN_FILE_BYTES) return null;
|
|
39
|
+
return fs.readFileSync(absolute, 'utf8');
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function listDirectory(absolute) {
|
|
46
|
+
try {
|
|
47
|
+
return fs
|
|
48
|
+
.readdirSync(absolute, { withFileTypes: true })
|
|
49
|
+
.filter((entry) => entry.isFile())
|
|
50
|
+
.map((entry) => entry.name)
|
|
51
|
+
.sort();
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Every arkgate pin under `root` (repo-relative files, 1-based lines).
|
|
59
|
+
* @param {string} root
|
|
60
|
+
* @param {{ observeInput?: (absolutePath: string, kind: string) => void }} [options]
|
|
61
|
+
*/
|
|
62
|
+
export function collectArkgatePins(root, { observeInput } = {}) {
|
|
63
|
+
const pins = [];
|
|
64
|
+
const probe = (rel, source) => {
|
|
65
|
+
const absolute = path.join(root, rel);
|
|
66
|
+
observeInput?.(absolute, 'arkgate-pin');
|
|
67
|
+
const text = readSmall(absolute);
|
|
68
|
+
if (text) pins.push(...parseArkgatePins(rel, text, source));
|
|
69
|
+
};
|
|
70
|
+
for (const [rel, source] of FIXED_CANDIDATES) probe(rel, source);
|
|
71
|
+
for (const [dir, source, accept] of DIRECTORY_CANDIDATES) {
|
|
72
|
+
const absoluteDir = path.join(root, dir);
|
|
73
|
+
observeInput?.(absoluteDir, 'arkgate-pin');
|
|
74
|
+
for (const name of listDirectory(absoluteDir)) {
|
|
75
|
+
if (accept(name)) probe(`${dir}/${name}`, source);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return pins;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The arkgate version running this check. */
|
|
82
|
+
export function runningArkgateVersion() {
|
|
83
|
+
const text = readSmall(path.join(__packageRoot, 'package.json'));
|
|
84
|
+
if (!text) return undefined;
|
|
85
|
+
try {
|
|
86
|
+
const pkg = JSON.parse(text);
|
|
87
|
+
return typeof pkg.version === 'string' ? pkg.version : undefined;
|
|
88
|
+
} catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
import fs from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { buildArkRuleFileHints } from './arkrules-sensors.mjs';
|
|
14
|
+
import { matchSimpleGlob } from './invariant-coverage-io.mjs';
|
|
14
15
|
|
|
15
16
|
/** Default hint-file budget. Same lever as `coverage.maxFiles`. */
|
|
16
17
|
export const DEFAULT_MAX_HINT_FILES = 400;
|
|
17
18
|
/** Hard ceiling — same clamp as coverage.maxFiles. */
|
|
18
|
-
|
|
19
|
+
const MAX_HINT_FILES_CAP = 20_000;
|
|
19
20
|
const MAX_FILE_BYTES = 256 * 1024;
|
|
20
21
|
|
|
21
22
|
const HINT_SENSORS = new Set([
|
|
@@ -37,17 +38,6 @@ export function needsArkRuleFileHints(arkRules) {
|
|
|
37
38
|
return (arkRules?.structure ?? []).some((rule) => HINT_SENSORS.has(rule?.sensor));
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
/**
|
|
41
|
-
* @param {unknown} maxFiles
|
|
42
|
-
* @returns {number}
|
|
43
|
-
*/
|
|
44
|
-
export function resolveHintBudget(maxFiles) {
|
|
45
|
-
if (Number.isInteger(maxFiles) && maxFiles > 0) {
|
|
46
|
-
return Math.min(maxFiles, MAX_HINT_FILES_CAP);
|
|
47
|
-
}
|
|
48
|
-
return DEFAULT_MAX_HINT_FILES;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
41
|
/**
|
|
52
42
|
* @param {unknown} hints
|
|
53
43
|
* @returns {null | {
|
|
@@ -74,41 +64,6 @@ export function formatHintBudgetDoctorLine(budget) {
|
|
|
74
64
|
return `${HINT_BUDGET_DOCTOR_LINE} Hinted ${budget.hinted} of ${budget.governed} eligible governed files (budget ${budget.budget}).`;
|
|
75
65
|
}
|
|
76
66
|
|
|
77
|
-
/**
|
|
78
|
-
* Minimal glob match (double-star slash = zero path segments).
|
|
79
|
-
* @param {string} glob
|
|
80
|
-
* @param {string} file
|
|
81
|
-
*/
|
|
82
|
-
function matchSimpleGlob(glob, file) {
|
|
83
|
-
const pattern = String(glob || '').replace(/\\/g, '/');
|
|
84
|
-
const target = String(file || '').replace(/\\/g, '/');
|
|
85
|
-
if (!pattern) return false;
|
|
86
|
-
let out = '';
|
|
87
|
-
for (let i = 0; i < pattern.length; i += 1) {
|
|
88
|
-
const c = pattern[i];
|
|
89
|
-
if (c === '*') {
|
|
90
|
-
if (pattern[i + 1] === '*') {
|
|
91
|
-
if (pattern[i + 2] === '/') {
|
|
92
|
-
out += '(?:.*/)?';
|
|
93
|
-
i += 2;
|
|
94
|
-
} else {
|
|
95
|
-
out += '.*';
|
|
96
|
-
i += 1;
|
|
97
|
-
}
|
|
98
|
-
} else {
|
|
99
|
-
out += '[^/]*';
|
|
100
|
-
}
|
|
101
|
-
} else if (c === '?') {
|
|
102
|
-
out += '[^/]';
|
|
103
|
-
} else if (/[.+^${}()|[\]\\]/.test(c)) {
|
|
104
|
-
out += `\\${c}`;
|
|
105
|
-
} else {
|
|
106
|
-
out += c;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return new RegExp(`^${out}$`).test(target);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
67
|
/**
|
|
113
68
|
* @param {string} file
|
|
114
69
|
* @param {unknown} appliesTo
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* Pure CLI helper (bin/lib/arkrules-contract.mjs). Zero Node I/O.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { isObject, validateSchemaNode, valueType } from './schema-validation.mjs';
|
|
11
12
|
export const ARK_RULES_SCHEMA_VERSION = '1.0';
|
|
12
|
-
|
|
13
|
+
const ARK_RULES_SCHEMA_URL = 'https://unpkg.com/arkgate/schemas/ark.arkrules.schema.json';
|
|
13
14
|
/** Closed sensor vocabulary — keep in lockstep with arkRulesTypes.ARK_RULE_SENSOR_IDS. */
|
|
14
15
|
export const ARK_RULE_SENSORS = [
|
|
15
16
|
'aggregate-private-state',
|
|
@@ -105,113 +106,6 @@ export class ArkRulesValidationError extends Error {
|
|
|
105
106
|
this.issues = issues;
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
|
-
function isObject(value) {
|
|
109
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
110
|
-
}
|
|
111
|
-
function propertyPath(parent, key) {
|
|
112
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
|
|
113
|
-
? `${parent}.${key}`
|
|
114
|
-
: `${parent}[${JSON.stringify(key)}]`;
|
|
115
|
-
}
|
|
116
|
-
function valueType(value) {
|
|
117
|
-
if (value === null)
|
|
118
|
-
return 'null';
|
|
119
|
-
if (Array.isArray(value))
|
|
120
|
-
return 'array';
|
|
121
|
-
return typeof value;
|
|
122
|
-
}
|
|
123
|
-
function resolveSchemaRef(ref, root) {
|
|
124
|
-
const prefix = '#/$defs/';
|
|
125
|
-
if (!ref.startsWith(prefix))
|
|
126
|
-
return undefined;
|
|
127
|
-
return root.$defs[ref.slice(prefix.length)];
|
|
128
|
-
}
|
|
129
|
-
function validateNode(value, schema, path, root, issues) {
|
|
130
|
-
if (schema.$ref) {
|
|
131
|
-
const referenced = resolveSchemaRef(schema.$ref, root);
|
|
132
|
-
if (!referenced) {
|
|
133
|
-
issues.push({ path, message: `schema reference ${schema.$ref} cannot be resolved` });
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
validateNode(value, referenced, path, root, issues);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (schema.const !== undefined && !Object.is(value, schema.const)) {
|
|
140
|
-
issues.push({ path, message: `must equal ${JSON.stringify(schema.const)}` });
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
if (schema.enum && !schema.enum.some((candidate) => Object.is(candidate, value))) {
|
|
144
|
-
issues.push({ path, message: `must be one of ${schema.enum.map(String).join(', ')}` });
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
if (schema.type === 'object') {
|
|
148
|
-
if (!isObject(value)) {
|
|
149
|
-
issues.push({ path, message: `must be an object; received ${valueType(value)}` });
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
const properties = schema.properties ?? {};
|
|
153
|
-
for (const key of schema.required ?? []) {
|
|
154
|
-
if (value[key] === undefined) {
|
|
155
|
-
issues.push({ path: propertyPath(path, key), message: 'is required' });
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
if (schema.additionalProperties === false) {
|
|
159
|
-
for (const key of Object.keys(value)) {
|
|
160
|
-
if (!(key in properties)) {
|
|
161
|
-
issues.push({ path: propertyPath(path, key), message: 'unknown field' });
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
else if (isObject(schema.additionalProperties)) {
|
|
166
|
-
const additional = schema.additionalProperties;
|
|
167
|
-
for (const key of Object.keys(value)) {
|
|
168
|
-
if (!(key in properties)) {
|
|
169
|
-
validateNode(value[key], additional, propertyPath(path, key), root, issues);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
for (const [key, childSchema] of Object.entries(properties)) {
|
|
174
|
-
if (value[key] !== undefined) {
|
|
175
|
-
validateNode(value[key], childSchema, propertyPath(path, key), root, issues);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
if (schema.type === 'array') {
|
|
181
|
-
if (!Array.isArray(value)) {
|
|
182
|
-
issues.push({ path, message: `must be an array; received ${valueType(value)}` });
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
186
|
-
issues.push({ path, message: `must contain at least ${schema.minItems} item(s)` });
|
|
187
|
-
}
|
|
188
|
-
if (schema.uniqueItems) {
|
|
189
|
-
const serialized = value.map((entry) => JSON.stringify(entry));
|
|
190
|
-
if (new Set(serialized).size !== serialized.length) {
|
|
191
|
-
issues.push({ path, message: 'must not contain duplicate items' });
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
if (schema.items) {
|
|
195
|
-
value.forEach((entry, index) => validateNode(entry, schema.items, `${path}[${index}]`, root, issues));
|
|
196
|
-
}
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
if (schema.type === 'string') {
|
|
200
|
-
if (typeof value !== 'string') {
|
|
201
|
-
issues.push({ path, message: `must be a string; received ${valueType(value)}` });
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
205
|
-
issues.push({ path, message: `must contain at least ${schema.minLength} character(s)` });
|
|
206
|
-
}
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
if (schema.type === 'boolean') {
|
|
210
|
-
if (typeof value !== 'boolean') {
|
|
211
|
-
issues.push({ path, message: `must be a boolean; received ${valueType(value)}` });
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
109
|
function defaultedArkRules(input) {
|
|
216
110
|
return {
|
|
217
111
|
...input,
|
|
@@ -299,7 +193,7 @@ export function loadArkRulesContract(input, source = 'arkrules.json', expectedLa
|
|
|
299
193
|
}
|
|
300
194
|
const candidate = defaultedArkRules(input);
|
|
301
195
|
const issues = [];
|
|
302
|
-
|
|
196
|
+
validateSchemaNode(candidate, ARK_RULES_SCHEMA, '$', ARK_RULES_SCHEMA, issues);
|
|
303
197
|
validateSemantics(candidate, issues);
|
|
304
198
|
if (expectedLayer !== undefined &&
|
|
305
199
|
typeof candidate.layer === 'string' &&
|
|
@@ -328,39 +222,136 @@ export function parseArkRulesJson(json, source = 'arkrules.json', expectedLayer)
|
|
|
328
222
|
}
|
|
329
223
|
return loadArkRulesContract(input, source, expectedLayer);
|
|
330
224
|
}
|
|
225
|
+
function effectiveRuleId(part, localId) {
|
|
226
|
+
return part.childId ? `${part.childId}#${localId}` : localId;
|
|
227
|
+
}
|
|
228
|
+
function effectiveAppliesTo(part, appliesTo) {
|
|
229
|
+
if (appliesTo && appliesTo.length > 0)
|
|
230
|
+
return [...appliesTo];
|
|
231
|
+
if (part.childId && part.defaultAppliesTo && part.defaultAppliesTo.length > 0) {
|
|
232
|
+
return [...part.defaultAppliesTo];
|
|
233
|
+
}
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
function provenanceFor(part, localId) {
|
|
237
|
+
return {
|
|
238
|
+
sourceFile: part.sourceFile,
|
|
239
|
+
ruleId: effectiveRuleId(part, localId),
|
|
240
|
+
layer: part.layer,
|
|
241
|
+
...(part.childId ? { childId: part.childId } : {}),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* True when every path the glob can match stays under the slice root.
|
|
246
|
+
* A wider prefix, a `**` that is not the last segment, or `..` escapes.
|
|
247
|
+
*/
|
|
248
|
+
function appliesToPatternInsideSlice(pattern, sliceRoot) {
|
|
249
|
+
const root = sliceRoot.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
|
|
250
|
+
const value = pattern.trim().replace(/\\/g, '/').replace(/^\.\//, '').toLowerCase();
|
|
251
|
+
if (!value || !root || value.includes('..'))
|
|
252
|
+
return false;
|
|
253
|
+
if (value === root || value === `${root}/**` || value === `${root}/*`)
|
|
254
|
+
return true;
|
|
255
|
+
if (value.startsWith(`${root}/`)) {
|
|
256
|
+
const parts = value.slice(root.length + 1).split('/');
|
|
257
|
+
const globStar = parts.indexOf('**');
|
|
258
|
+
if (globStar !== -1 && globStar !== parts.length - 1)
|
|
259
|
+
return false;
|
|
260
|
+
return parts.every((part) => part.length > 0);
|
|
261
|
+
}
|
|
262
|
+
const bareRoot = root.replace(/^(?:src|app)\//, '');
|
|
263
|
+
const bareValue = value.replace(/^(?:src|app)\//, '');
|
|
264
|
+
if (bareRoot !== root || bareValue !== value)
|
|
265
|
+
return appliesToPatternInsideSlice(bareValue, bareRoot);
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
/** Explicit appliesTo on a discovered file that can match outside its slice. */
|
|
269
|
+
export function sliceScopeEscapes(part) {
|
|
270
|
+
if (!part.childId || !part.defaultAppliesTo?.length)
|
|
271
|
+
return [];
|
|
272
|
+
const sliceRoot = String(part.defaultAppliesTo[0]).replace(/\/\*\*$/, '');
|
|
273
|
+
const escapes = [];
|
|
274
|
+
const entries = [...(part.file.structure ?? []), ...(part.file.invariants ?? [])];
|
|
275
|
+
for (const entry of entries) {
|
|
276
|
+
for (const pattern of entry.appliesTo ?? []) {
|
|
277
|
+
if (!appliesToPatternInsideSlice(pattern, sliceRoot))
|
|
278
|
+
escapes.push({ id: entry.id, pattern });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return escapes;
|
|
282
|
+
}
|
|
283
|
+
/** Effective ids declared by more than one part. Central ids stay bare; slice ids are namespaced. */
|
|
284
|
+
export function duplicateArkRuleIds(parts) {
|
|
285
|
+
const seen = new Map();
|
|
286
|
+
const dupes = new Map();
|
|
287
|
+
for (const part of parts) {
|
|
288
|
+
const localIds = [
|
|
289
|
+
...(part.file.structure ?? []).map((entry) => entry.id),
|
|
290
|
+
...(part.file.invariants ?? []).map((entry) => entry.id),
|
|
291
|
+
];
|
|
292
|
+
for (const localId of localIds) {
|
|
293
|
+
const id = effectiveRuleId(part, localId);
|
|
294
|
+
const key = `${part.layer}\0${id}`;
|
|
295
|
+
const existing = seen.get(key);
|
|
296
|
+
if (!existing) {
|
|
297
|
+
seen.set(key, { id, layer: part.layer, sourceFiles: [part.sourceFile] });
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const row = dupes.get(key) ?? { id, layer: part.layer, sourceFiles: [...existing.sourceFiles] };
|
|
301
|
+
if (!row.sourceFiles.includes(part.sourceFile))
|
|
302
|
+
row.sourceFiles.push(part.sourceFile);
|
|
303
|
+
dupes.set(key, row);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return [...dupes.values()];
|
|
307
|
+
}
|
|
331
308
|
/**
|
|
332
309
|
* Build the Effective Contract from already-validated ArkRules files.
|
|
333
|
-
*
|
|
310
|
+
* Same-layer parts merge. A child id namespaces the effective id and does not rewrite the file.
|
|
334
311
|
*/
|
|
335
312
|
export function buildEffectiveArkRules(parts) {
|
|
336
313
|
const byLayer = {};
|
|
337
314
|
const structure = [];
|
|
338
315
|
const invariants = [];
|
|
339
|
-
const ordered = [...parts].sort((a, b) => a.layer.localeCompare(b.layer));
|
|
316
|
+
const ordered = [...parts].sort((a, b) => a.layer.localeCompare(b.layer) || a.sourceFile.localeCompare(b.sourceFile));
|
|
340
317
|
for (const part of ordered) {
|
|
341
|
-
const structureRules = (part.file.structure ?? []).map((entry) =>
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
318
|
+
const structureRules = (part.file.structure ?? []).map((entry) => {
|
|
319
|
+
const appliesTo = effectiveAppliesTo(part, entry.appliesTo);
|
|
320
|
+
return {
|
|
321
|
+
...entry,
|
|
322
|
+
id: effectiveRuleId(part, entry.id),
|
|
323
|
+
...(appliesTo ? { appliesTo } : { appliesTo: undefined }),
|
|
324
|
+
mode: normalizeMode(entry.mode),
|
|
325
|
+
provenance: provenanceFor(part, entry.id),
|
|
326
|
+
};
|
|
327
|
+
});
|
|
328
|
+
const invariantRules = (part.file.invariants ?? []).map((entry) => {
|
|
329
|
+
const appliesTo = effectiveAppliesTo(part, entry.appliesTo);
|
|
330
|
+
return {
|
|
331
|
+
...entry,
|
|
332
|
+
id: effectiveRuleId(part, entry.id),
|
|
333
|
+
...(appliesTo ? { appliesTo } : { appliesTo: undefined }),
|
|
334
|
+
mode: normalizeMode(entry.mode),
|
|
335
|
+
provenance: provenanceFor(part, entry.id),
|
|
336
|
+
};
|
|
337
|
+
});
|
|
338
|
+
const bucket = byLayer[part.layer];
|
|
339
|
+
if (!bucket) {
|
|
340
|
+
byLayer[part.layer] = {
|
|
354
341
|
sourceFile: part.sourceFile,
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
342
|
+
sourceFiles: [part.sourceFile],
|
|
343
|
+
structure: structureRules,
|
|
344
|
+
invariants: invariantRules,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
if (!bucket.sourceFiles)
|
|
349
|
+
bucket.sourceFiles = [bucket.sourceFile];
|
|
350
|
+
if (!bucket.sourceFiles.includes(part.sourceFile))
|
|
351
|
+
bucket.sourceFiles.push(part.sourceFile);
|
|
352
|
+
bucket.structure.push(...structureRules);
|
|
353
|
+
bucket.invariants.push(...invariantRules);
|
|
354
|
+
}
|
|
364
355
|
structure.push(...structureRules);
|
|
365
356
|
invariants.push(...invariantRules);
|
|
366
357
|
}
|