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.
Files changed (201) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/README.md +7 -5
  3. package/bin/ark-check-runtime.mjs +41 -91
  4. package/bin/ark-dashboard.mjs +56 -53
  5. package/bin/ark-layer-match.mjs +707 -121
  6. package/bin/ark-mcp-runtime.mjs +448 -169
  7. package/bin/ark-mcp.mjs +68 -4
  8. package/bin/ark-shared.mjs +42 -22
  9. package/bin/ark.mjs +65 -97
  10. package/bin/lib/adapter-contract-types.mjs +5 -0
  11. package/bin/lib/adapter-contract.mjs +15 -0
  12. package/bin/lib/adoption-stance.mjs +2 -2
  13. package/bin/lib/adr-path.mjs +3 -3
  14. package/bin/lib/agent-gates.mjs +0 -61
  15. package/bin/lib/agent-homes.mjs +12 -21
  16. package/bin/lib/agent-projection-command.mjs +9 -5
  17. package/bin/lib/ambient-state.mjs +9 -30
  18. package/bin/lib/analysis-engine.mjs +15 -11
  19. package/bin/lib/architecture-scan.mjs +61 -15
  20. package/bin/lib/ark-order-doctor.mjs +4 -29
  21. package/bin/lib/ark-order-facts.mjs +319 -17
  22. package/bin/lib/ark-order-sensors.mjs +28 -101
  23. package/bin/lib/ark-order-types.mjs +3 -2
  24. package/bin/lib/ark-run-doctor.mjs +17 -28
  25. package/bin/lib/ark-run-facts.mjs +461 -48
  26. package/bin/lib/ark-run-sensors.mjs +6 -4
  27. package/bin/lib/arkgate-pins.mjs +91 -0
  28. package/bin/lib/arkrule-file-hints.mjs +2 -47
  29. package/bin/lib/arkrules-contract.mjs +124 -133
  30. package/bin/lib/arkrules-sensors.mjs +55 -252
  31. package/bin/lib/check-args.mjs +85 -1
  32. package/bin/lib/ci-and-commands.mjs +24 -78
  33. package/bin/lib/ci-merge-boundary.mjs +1 -1
  34. package/bin/lib/class-source-scan.mjs +479 -0
  35. package/bin/lib/clone-detection.mjs +474 -0
  36. package/bin/lib/codex-home.mjs +5 -3
  37. package/bin/lib/config-contract-slices.mjs +739 -0
  38. package/bin/lib/config-contract.mjs +42 -607
  39. package/bin/lib/config-extras.mjs +16 -22
  40. package/bin/lib/config-warnings.mjs +32 -0
  41. package/bin/lib/contract-smells.mjs +33 -15
  42. package/bin/lib/core-ratchet.mjs +0 -2
  43. package/bin/lib/dashboard-cli.mjs +62 -0
  44. package/bin/lib/deep-module-coach.mjs +2 -4
  45. package/bin/lib/deploy-path.mjs +13 -38
  46. package/bin/lib/design-delta.mjs +1 -1
  47. package/bin/lib/design-smells.mjs +2 -2
  48. package/bin/lib/diagnostic-catalog.mjs +23 -9
  49. package/bin/lib/doctor-advisories.mjs +78 -8
  50. package/bin/lib/doctor-green-cite.mjs +1 -1
  51. package/bin/lib/doctor-human.mjs +1 -1
  52. package/bin/lib/doctor-next-actions.mjs +19 -4
  53. package/bin/lib/doctor-plan.mjs +9 -13
  54. package/bin/lib/duplication-io.mjs +572 -0
  55. package/bin/lib/effective-contract-load.mjs +282 -83
  56. package/bin/lib/enforcement-profiles.mjs +40 -4
  57. package/bin/lib/entry-points-io.mjs +424 -0
  58. package/bin/lib/extra-merge-teeth.mjs +36 -0
  59. package/bin/lib/field-install.mjs +38 -14
  60. package/bin/lib/first-run-help.mjs +10 -5
  61. package/bin/lib/flat-parent-importers.mjs +139 -0
  62. package/bin/lib/flat-parent-pilot.mjs +40 -100
  63. package/bin/lib/gate-files.mjs +58 -90
  64. package/bin/lib/git-change-scope.mjs +109 -0
  65. package/bin/lib/github-enforcement.mjs +166 -29
  66. package/bin/lib/golden-pattern.mjs +1 -1
  67. package/bin/lib/hook-templates.mjs +17 -15
  68. package/bin/lib/host-support-matrix.mjs +3 -1
  69. package/bin/lib/html-report-advisories.mjs +4 -0
  70. package/bin/lib/html-report-depth.mjs +2 -2
  71. package/bin/lib/html-report.mjs +2 -2
  72. package/bin/lib/import-graph-projection.mjs +112 -0
  73. package/bin/lib/import-resolve.mjs +13 -1
  74. package/bin/lib/improvement-compass-doctor.mjs +1 -24
  75. package/bin/lib/install-migrate.mjs +50 -73
  76. package/bin/lib/invariant-coverage-io.mjs +4 -3
  77. package/bin/lib/invariant-coverage.mjs +85 -11
  78. package/bin/lib/invariant-probe-cli.mjs +347 -0
  79. package/bin/lib/invariant-probe-io.mjs +165 -0
  80. package/bin/lib/invariant-probe-runner.mjs +268 -0
  81. package/bin/lib/invariant-probe-sites.mjs +174 -0
  82. package/bin/lib/invariant-probe-workspace.mjs +183 -0
  83. package/bin/lib/invariant-probe.mjs +461 -0
  84. package/bin/lib/invariant-tests-path.mjs +16 -22
  85. package/bin/lib/literal-path-drift.mjs +4 -2
  86. package/bin/lib/managed-upgrade.mjs +2 -8
  87. package/bin/lib/mcp-adoption.mjs +13 -6
  88. package/bin/lib/mcp-hook-payload.mjs +403 -63
  89. package/bin/lib/orphan-modules-io.mjs +458 -0
  90. package/bin/lib/orphan-modules.mjs +251 -0
  91. package/bin/lib/outside-importers.mjs +197 -0
  92. package/bin/lib/package-manager.mjs +60 -2
  93. package/bin/lib/parse-health.mjs +1 -1
  94. package/bin/lib/passthrough-commands.mjs +83 -0
  95. package/bin/lib/persistence-write-hint.mjs +106 -0
  96. package/bin/lib/pin-dependent-gates.mjs +117 -0
  97. package/bin/lib/policy-delta-io.mjs +219 -14
  98. package/bin/lib/post-green-path.mjs +1 -1
  99. package/bin/lib/prepare-change.mjs +115 -7
  100. package/bin/lib/prepare-write.mjs +2 -2
  101. package/bin/lib/presets.mjs +59 -73
  102. package/bin/lib/product-copy.mjs +1 -6
  103. package/bin/lib/project-root.mjs +31 -62
  104. package/bin/lib/remediation.mjs +23 -4
  105. package/bin/lib/resolved-candidate-facts.mjs +405 -117
  106. package/bin/lib/rules-inventory.mjs +22 -3
  107. package/bin/lib/rules-under-contract.mjs +247 -9
  108. package/bin/lib/scan-files.mjs +36 -4
  109. package/bin/lib/schema-validation.mjs +130 -0
  110. package/bin/lib/sensor-promote-cli.mjs +2 -11
  111. package/bin/lib/sensor-promote-io.mjs +33 -22
  112. package/bin/lib/sensor-promotion.mjs +1 -1
  113. package/bin/lib/skill-catalog-apply.mjs +1 -1
  114. package/bin/lib/skill-install.mjs +42 -38
  115. package/bin/lib/snippet-analysis.mjs +140 -6
  116. package/bin/lib/source-policy.mjs +34 -0
  117. package/bin/lib/start-install-recovery.mjs +72 -2
  118. package/bin/lib/start-preview.mjs +31 -1
  119. package/bin/lib/states-transitions-presence.mjs +3 -3
  120. package/bin/lib/status-command.mjs +35 -22
  121. package/bin/lib/status-manifest.mjs +50 -30
  122. package/bin/lib/status-transition-catalog.mjs +0 -6
  123. package/bin/lib/suggestions.mjs +3 -3
  124. package/bin/lib/team-parliament-io.mjs +76 -88
  125. package/bin/lib/team-parliament.mjs +21 -10
  126. package/bin/lib/upgrade-behavior-changes.mjs +2 -2
  127. package/bin/lib/upgrade-command.mjs +2 -24
  128. package/bin/lib/upgrade-outcome.mjs +1 -1
  129. package/bin/lib/upstream-report.mjs +21 -10
  130. package/bin/lib/violations.mjs +10 -9
  131. package/bin/lib/weakest-link.mjs +13 -0
  132. package/bin/lib/write-path-capabilities.mjs +6 -4
  133. package/dist/chunk-A5EIY3HI.cjs +5 -0
  134. package/dist/chunk-AHACEKSV.js +16 -0
  135. package/dist/chunk-SVSQVGFJ.js +5 -0
  136. package/dist/chunk-UN5EH4QV.cjs +16 -0
  137. package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-GPvvdleM.d.cts} +30 -4
  138. package/dist/configTypes-GPvvdleM.d.ts +257 -0
  139. package/dist/eslint/index.cjs +18 -5
  140. package/dist/eslint/index.d.cts +368 -0
  141. package/dist/eslint/index.d.ts +139 -156
  142. package/dist/eslint/index.js +11 -5
  143. package/dist/index.cjs +36 -35
  144. package/dist/index.d.cts +5547 -0
  145. package/dist/index.d.ts +3283 -198
  146. package/dist/index.js +36 -35
  147. package/dist/inspectorListen-6DQVJ4DC.js +1 -0
  148. package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
  149. package/dist/nestjs/index.cjs +1 -18
  150. package/dist/nestjs/index.d.cts +22 -0
  151. package/dist/nestjs/index.d.ts +1 -3
  152. package/dist/nestjs/index.js +1 -18
  153. package/dist/order/index.cjs +1 -1
  154. package/dist/order/index.d.cts +155 -0
  155. package/dist/order/index.d.ts +15 -3
  156. package/dist/order/index.js +1 -1
  157. package/dist/runtime/index.cjs +13 -30
  158. package/dist/{diagnosticCatalog-35mrwAHe.d.ts → runtime/index.d.cts} +828 -276
  159. package/dist/runtime/index.d.ts +2741 -133
  160. package/dist/runtime/index.js +13 -30
  161. package/dist/{types-Bdj32QwA.d.ts → types-CnauMQQL.d.cts} +607 -163
  162. package/dist/types-CnauMQQL.d.ts +1800 -0
  163. package/docs/README.md +3 -1
  164. package/docs/agent-guide.md +217 -16
  165. package/docs/ai-gates.md +174 -26
  166. package/docs/arkorder.md +32 -15
  167. package/docs/brownfield-adoption.md +5 -1
  168. package/docs/configuration.md +230 -31
  169. package/docs/develop.md +2 -1
  170. package/docs/diagnostics.md +212 -19
  171. package/docs/package-surface.md +35 -9
  172. package/docs/product-voice.md +7 -1
  173. package/package.json +69 -19
  174. package/schemas/ark.analysis-result.schema.json +15 -0
  175. package/schemas/ark.config.schema.json +77 -12
  176. package/schemas/ark.invariant-probe.schema.json +368 -0
  177. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  178. package/schemas/ark.status-manifest.schema.json +22 -0
  179. package/server.json +3 -9
  180. package/templates/agent-skills/README.md +1 -1
  181. package/templates/agent-skills/ark-adopt/SKILL.md +5 -1
  182. package/templates/agent-skills/ark-contract/SKILL.md +17 -0
  183. package/templates/agent-skills/ark-coverage/SKILL.md +2 -0
  184. package/templates/agent-skills/ark-explore/SKILL.md +7 -0
  185. package/templates/agent-skills/ark-fix/SKILL.md +20 -0
  186. package/templates/agent-skills/ark-order/SKILL.md +4 -2
  187. package/templates/agent-skills/ark-place/SKILL.md +8 -0
  188. package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
  189. package/templates/arkrules/PersistenceAdapters.json +1 -1
  190. package/templates/arkrules/PresentationAdapters.json +1 -1
  191. package/templates/skills/ark-adopt.md +5 -1
  192. package/templates/skills/ark-contract.md +17 -0
  193. package/templates/skills/ark-coverage.md +2 -0
  194. package/templates/skills/ark-explore.md +7 -0
  195. package/templates/skills/ark-fix.md +20 -0
  196. package/templates/skills/ark-order.md +4 -2
  197. package/templates/skills/ark-place.md +8 -0
  198. package/templates/skills/ark-runtime.md +6 -1
  199. package/bin/lib/ark-order-error.mjs +0 -18
  200. package/bin/lib/ark-order-invariants.mjs +0 -346
  201. 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
- export const MAX_HINT_FILES_CAP = 20_000;
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
- export const ARK_RULES_SCHEMA_URL = 'https://unpkg.com/arkgate/schemas/ark.arkrules.schema.json';
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
- validateNode(candidate, ARK_RULES_SCHEMA, '$', ARK_RULES_SCHEMA, issues);
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
- * Callers supply `{ layer → { sourceFile, file } }` after resolving references.
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
- ...entry,
343
- mode: normalizeMode(entry.mode),
344
- provenance: {
345
- sourceFile: part.sourceFile,
346
- ruleId: entry.id,
347
- layer: part.layer,
348
- },
349
- }));
350
- const invariantRules = (part.file.invariants ?? []).map((entry) => ({
351
- ...entry,
352
- mode: normalizeMode(entry.mode),
353
- provenance: {
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
- ruleId: entry.id,
356
- layer: part.layer,
357
- },
358
- }));
359
- byLayer[part.layer] = {
360
- sourceFile: part.sourceFile,
361
- structure: structureRules,
362
- invariants: invariantRules,
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
  }