arkgate 4.7.5 → 4.8.0

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 (63) hide show
  1. package/CHANGELOG.md +39 -3
  2. package/README.md +24 -12
  3. package/bin/lib/analysis-engine.mjs +6 -6
  4. package/bin/lib/ark-order-error.mjs +18 -0
  5. package/bin/lib/ark-order-facts.mjs +53 -0
  6. package/bin/lib/ark-order-invariants.mjs +160 -0
  7. package/bin/lib/ark-order-sensors.mjs +118 -0
  8. package/bin/lib/ark-order-types.mjs +11 -0
  9. package/bin/lib/ark-run-sensors.mjs +13 -5
  10. package/bin/lib/config-contract.mjs +16 -72
  11. package/bin/lib/config-extras.mjs +151 -0
  12. package/bin/lib/diagnostic-catalog.mjs +7 -2
  13. package/bin/lib/extra-merge-teeth.mjs +8 -2
  14. package/bin/lib/install-migrate.mjs +4 -2
  15. package/bin/lib/managed-upgrade.mjs +1 -1
  16. package/bin/lib/mcp-adoption.mjs +9 -3
  17. package/bin/lib/remediation.mjs +48 -9
  18. package/bin/lib/resolved-candidate-facts.mjs +43 -0
  19. package/bin/lib/skill-install.mjs +17 -2
  20. package/bin/lib/start-preview.mjs +1 -0
  21. package/bin/lib/status-manifest.mjs +1 -1
  22. package/bin/lib/write-path-capabilities.mjs +2 -2
  23. package/dist/{configTypes-CgJimx9o.d.ts → configTypes-BdCe_gvv.d.ts} +22 -6
  24. package/dist/diagnosticCatalog-RiKPUFRG.d.ts +2307 -0
  25. package/dist/eslint/index.cjs +7 -6
  26. package/dist/eslint/index.d.ts +33 -2
  27. package/dist/eslint/index.js +7 -6
  28. package/dist/index.cjs +35 -35
  29. package/dist/index.d.ts +271 -2554
  30. package/dist/index.js +35 -35
  31. package/dist/nestjs/index.cjs +18 -0
  32. package/dist/nestjs/index.d.ts +24 -0
  33. package/dist/nestjs/index.js +18 -0
  34. package/dist/order/index.cjs +1 -0
  35. package/dist/order/index.d.ts +79 -0
  36. package/dist/order/index.js +1 -0
  37. package/dist/runtime/index.cjs +25 -0
  38. package/dist/runtime/index.d.ts +497 -0
  39. package/dist/runtime/index.js +25 -0
  40. package/dist/types-C9KApBzX.d.ts +1237 -0
  41. package/dist/types-DCSlrRnV.d.ts +181 -0
  42. package/docs/README.md +4 -5
  43. package/docs/agent-guide.md +1 -1
  44. package/docs/ai-gates.md +9 -2
  45. package/docs/configuration.md +13 -6
  46. package/docs/develop.md +12 -3
  47. package/docs/diagnostics.md +57 -3
  48. package/docs/package-surface.md +21 -17
  49. package/docs/product-voice.md +2 -1
  50. package/docs/use.md +8 -0
  51. package/package.json +21 -2
  52. package/schemas/ark.config.schema.json +54 -2
  53. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  54. package/server.json +2 -2
  55. package/templates/agent-skills/README.md +1 -1
  56. package/templates/agent-skills/ark-adopt/SKILL.md +2 -2
  57. package/templates/agent-skills/ark-contract/SKILL.md +1 -1
  58. package/templates/agent-skills/ark-place/SKILL.md +15 -6
  59. package/templates/agent-skills/ark-runtime/SKILL.md +10 -15
  60. package/templates/skills/ark-adopt.md +2 -2
  61. package/templates/skills/ark-contract.md +1 -1
  62. package/templates/skills/ark-place.md +15 -6
  63. package/templates/skills/ark-runtime.md +10 -15
@@ -0,0 +1,18 @@
1
+ /**
2
+ * GENERATED FILE — do not edit by hand.
3
+ *
4
+ * Canonical algorithm: src/domain/arkOrderError.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/ark-order-error.mjs). Zero Node I/O.
9
+ */
10
+
11
+ export class ArkOrderError extends Error {
12
+ code;
13
+ constructor(code, message) {
14
+ super(message);
15
+ this.name = 'ArkOrderError';
16
+ this.code = code;
17
+ }
18
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * GENERATED FILE — do not edit by hand.
3
+ *
4
+ * Canonical algorithm: src/domain/arkOrderFacts.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/ark-order-facts.mjs). Zero Node I/O.
9
+ */
10
+
11
+ export const ARKORDER_PLANE_FACTORY = 'createOrderPlane';
12
+ export const ARKORDER_FORBIDDEN_METHODS = ['update', 'patch', 'set', 'mutate'];
13
+ export function isArkOrderModuleSpecifier(specifier) {
14
+ return specifier === 'arkgate/order' || specifier.startsWith('arkgate/order/');
15
+ }
16
+ function lineAt(content, index) {
17
+ let line = 1;
18
+ for (let i = 0; i < index && i < content.length; i += 1) {
19
+ if (content[i] === '\n')
20
+ line += 1;
21
+ }
22
+ return line;
23
+ }
24
+ function stripCommentsPreservingLines(content) {
25
+ return content
26
+ .replace(/\/\*[\s\S]*?\*\//g, (block) => block.replace(/[^\n]/g, ' '))
27
+ .replace(/(^|[^:])\/\/.*$/gm, (line) => line.replace(/\/\/.*$/, (c) => ' '.repeat(c.length)));
28
+ }
29
+ export function extractArkOrderPlaneCallsFromSource(file, content) {
30
+ const source = stripCommentsPreservingLines(content);
31
+ const facts = [];
32
+ const re = /\bcreateOrderPlane\s*(?:<[^>]*>)?\s*\(/g;
33
+ let match;
34
+ while ((match = re.exec(source)) !== null) {
35
+ facts.push({ file, line: lineAt(content, match.index), callee: ARKORDER_PLANE_FACTORY });
36
+ }
37
+ return facts;
38
+ }
39
+ export function extractArkOrderGenericUpdatesFromSource(file, content) {
40
+ const source = stripCommentsPreservingLines(content);
41
+ const facts = [];
42
+ const re = /\.((?:update|patch|set|mutate))\s*\(/g;
43
+ let match;
44
+ while ((match = re.exec(source)) !== null) {
45
+ const method = match[1];
46
+ const before = source.slice(Math.max(0, match.index - 80), match.index);
47
+ if (!/\b(?:plane|orderPlane|order)\s*$/.test(before) && !/\bcreateOrderPlane\b/.test(source)) {
48
+ continue;
49
+ }
50
+ facts.push({ file, line: lineAt(content, match.index), method });
51
+ }
52
+ return facts;
53
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * GENERATED FILE — do not edit by hand.
3
+ *
4
+ * Canonical algorithm: src/domain/arkOrderInvariants.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/ark-order-invariants.mjs). Zero Node I/O.
9
+ */
10
+
11
+ import { ArkOrderError } from './ark-order-error.mjs';
12
+ import { DEFAULT_MAX_XI_KEYS } from './ark-order-types.mjs';
13
+ import { deterministicHash, stableSerialize } from './stableHash';
14
+ export { DEFAULT_MAX_XI_KEYS };
15
+ const FORBIDDEN_PLANE_METHODS = ['update', 'patch', 'set', 'mutate'];
16
+ export function isForbiddenPlaneMethod(name) {
17
+ return FORBIDDEN_PLANE_METHODS.includes(name);
18
+ }
19
+ function isPrimitive(value) {
20
+ return (value === null ||
21
+ typeof value === 'string' ||
22
+ typeof value === 'number' ||
23
+ typeof value === 'boolean');
24
+ }
25
+ export function freezeRecord(input, label) {
26
+ const keys = Object.keys(input);
27
+ if (keys.length === 0 && label === 'ξ') {
28
+ throw new ArkOrderError('ARKORDER_EMPTY_XI', 'ξ must name at least one slow mode');
29
+ }
30
+ const out = {};
31
+ for (const key of keys.sort()) {
32
+ const value = input[key];
33
+ if (!isPrimitive(value)) {
34
+ throw new ArkOrderError('ARKORDER_NESTED_XI', `${label} key ${JSON.stringify(key)} is not a slow primitive (nested values are microstate)`);
35
+ }
36
+ out[key] = value;
37
+ }
38
+ return Object.freeze(out);
39
+ }
40
+ export function assertXiKeyCap(xi, maxXiKeys) {
41
+ const n = Object.keys(xi).length;
42
+ if (n === 0) {
43
+ throw new ArkOrderError('ARKORDER_EMPTY_XI', 'ξ must name at least one slow mode');
44
+ }
45
+ if (n > maxXiKeys) {
46
+ throw new ArkOrderError('ARKORDER_TOO_MANY_PARAMS', `ξ has ${n} keys; maxXiKeys is ${maxXiKeys} (Haken: few slow modes)`);
47
+ }
48
+ }
49
+ export function assertXiSchema(xi, schema) {
50
+ if (!schema)
51
+ return;
52
+ const properties = schema.properties ?? {};
53
+ const additional = schema.additionalProperties !== false ? true : false;
54
+ for (const key of Object.keys(xi)) {
55
+ const prop = properties[key];
56
+ if (!prop) {
57
+ if (!additional) {
58
+ throw new ArkOrderError('ARKORDER_SCHEMA', `ξ key ${JSON.stringify(key)} is not in xiSchema.properties`);
59
+ }
60
+ continue;
61
+ }
62
+ if (prop.enum && !prop.enum.some((allowed) => allowed === xi[key])) {
63
+ throw new ArkOrderError('ARKORDER_SCHEMA', `ξ key ${JSON.stringify(key)} value is not in enum`);
64
+ }
65
+ if (prop.type === 'null' && xi[key] !== null) {
66
+ throw new ArkOrderError('ARKORDER_SCHEMA', `ξ key ${JSON.stringify(key)} must be null`);
67
+ }
68
+ if (prop.type && prop.type !== 'null' && typeof xi[key] !== prop.type) {
69
+ throw new ArkOrderError('ARKORDER_SCHEMA', `ξ key ${JSON.stringify(key)} must be ${prop.type}`);
70
+ }
71
+ }
72
+ }
73
+ export function hashReleasePayload(xi, sigma) {
74
+ return deterministicHash(stableSerialize({ xi, sigma }));
75
+ }
76
+ export function createFrozenRelease(input) {
77
+ assertXiKeyCap(input.xi, input.maxXiKeys);
78
+ const xi = freezeRecord(input.xi, 'ξ');
79
+ assertXiSchema(xi, input.xiSchema);
80
+ const sigma = freezeRecord(input.sigma ?? {}, 'σ');
81
+ const release = Object.freeze({
82
+ version: input.version,
83
+ hash: hashReleasePayload(xi, sigma),
84
+ xi,
85
+ sigma,
86
+ releasedAt: input.now,
87
+ });
88
+ return release;
89
+ }
90
+ export function classifyIngest(projection, event, packs = []) {
91
+ const kind = event.kind;
92
+ for (const pack of packs) {
93
+ if (pack.escalateKinds?.includes(kind)) {
94
+ return {
95
+ kind: 'escalate',
96
+ event,
97
+ reason: `pack ${pack.id} slaves kind ${JSON.stringify(kind)} to a pattern change`,
98
+ };
99
+ }
100
+ }
101
+ if (projection.allowedKinds.includes(kind)) {
102
+ return { kind: 'absorb', event };
103
+ }
104
+ return {
105
+ kind: 'escalate',
106
+ event,
107
+ reason: `kind ${JSON.stringify(kind)} is not allowed by h(ξ); field cannot rewrite the pattern`,
108
+ };
109
+ }
110
+ export function blastRadiusOf(previous, next) {
111
+ const prev = new Set(previous.allowedKinds);
112
+ const nxt = new Set(next.allowedKinds);
113
+ const blast = new Set();
114
+ for (const kind of prev) {
115
+ if (!nxt.has(kind))
116
+ blast.add(kind);
117
+ }
118
+ for (const kind of nxt) {
119
+ if (!prev.has(kind))
120
+ blast.add(kind);
121
+ }
122
+ for (const item of next.invalidated)
123
+ blast.add(item);
124
+ return {
125
+ blastRadius: [...blast].sort(),
126
+ invalidations: [...next.invalidated].sort(),
127
+ };
128
+ }
129
+ export function proposePatternChange(input) {
130
+ const merged = { ...input.current.xi };
131
+ for (const [key, value] of Object.entries(input.delta)) {
132
+ if (value === undefined) {
133
+ delete merged[key];
134
+ continue;
135
+ }
136
+ merged[key] = value;
137
+ }
138
+ const candidate = createFrozenRelease({
139
+ xi: merged,
140
+ sigma: { ...input.current.sigma },
141
+ version: input.current.version + 1,
142
+ now: input.now,
143
+ maxXiKeys: input.maxXiKeys,
144
+ xiSchema: input.xiSchema,
145
+ });
146
+ if (candidate.hash === input.current.hash) {
147
+ throw new ArkOrderError('ARKORDER_EMPTY_BLAST', 'delta does not change ξ; that is not a pattern change');
148
+ }
149
+ const previous = input.projector(input.current, input.current.sigma);
150
+ const next = input.projector(candidate, candidate.sigma);
151
+ const { blastRadius, invalidations } = blastRadiusOf(previous, next);
152
+ if (blastRadius.length === 0) {
153
+ throw new ArkOrderError('ARKORDER_EMPTY_BLAST', 'pattern change has empty blast radius; that key is not an order parameter');
154
+ }
155
+ return {
156
+ nextXi: candidate.xi,
157
+ blastRadius,
158
+ invalidations,
159
+ };
160
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * GENERATED FILE — do not edit by hand.
3
+ *
4
+ * Canonical algorithm: src/domain/arkOrderSensors.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/ark-order-sensors.mjs). Zero Node I/O.
9
+ */
10
+
11
+ import { extractArkOrderGenericUpdatesFromSource, extractArkOrderPlaneCallsFromSource, isArkOrderModuleSpecifier, } from './ark-order-facts.mjs';
12
+ import { extraMergeTeethAllowed, } from './extra-merge-teeth.mjs';
13
+ import { deterministicNextAction } from './remediation.mjs';
14
+ export const ARKORDER_TIER1_SENSOR_IDS = [
15
+ 'arkorder-missing-plane',
16
+ 'arkorder-kernel-in-domain',
17
+ 'arkorder-generic-update',
18
+ 'arkorder-too-many-params',
19
+ 'arkorder-ingest-writes-xi',
20
+ ];
21
+ export const ARKORDER_RULE_IDS = {
22
+ 'arkorder-missing-plane': 'ARKORDER_MISSING_PLANE',
23
+ 'arkorder-kernel-in-domain': 'ARKORDER_KERNEL_IN_DOMAIN',
24
+ 'arkorder-generic-update': 'ARKORDER_GENERIC_UPDATE',
25
+ 'arkorder-too-many-params': 'ARKORDER_TOO_MANY_PARAMS',
26
+ 'arkorder-ingest-writes-xi': 'ARKORDER_INGEST_WRITES_XI',
27
+ };
28
+ function isDomainRoleLayer(layer, intentPrefixes = []) {
29
+ const name = layer.trim();
30
+ if (/^domain(?:model)?$/i.test(name) || /^domain(?=[A-Z_\-\s])/i.test(name))
31
+ return true;
32
+ return intentPrefixes.some((prefix) => {
33
+ const normalized = prefix.trim().replace(/\.+$/, '');
34
+ return normalized === 'Domain' || normalized.startsWith('Domain.');
35
+ });
36
+ }
37
+ function finding(extra, sensor, file, line, message, extras, teethAllowed) {
38
+ const failsStrict = extra.mode === 'enforced' && teethAllowed;
39
+ return {
40
+ ruleId: ARKORDER_RULE_IDS[sensor],
41
+ sensor,
42
+ message,
43
+ file,
44
+ line,
45
+ ...(extras?.fromLayer ? { fromLayer: extras.fromLayer } : {}),
46
+ ...(extras?.target ? { target: extras.target } : {}),
47
+ severity: failsStrict ? 'error' : 'warning',
48
+ failsStrict,
49
+ nextAction: deterministicNextAction({
50
+ ruleId: ARKORDER_RULE_IDS[sensor],
51
+ fromLayer: extras?.fromLayer,
52
+ target: extras?.target,
53
+ }),
54
+ };
55
+ }
56
+ export function evaluateArkOrderSensors(input) {
57
+ const extra = input.arkOrder;
58
+ if (!extra)
59
+ return { findings: [], completenessReasons: [] };
60
+ const teethAllowed = extraMergeTeethAllowed(input.classification);
61
+ const findings = [];
62
+ const roots = extra.planeRoots;
63
+ if (extra.mode === 'enforced' && roots.length === 0) {
64
+ findings.push(finding(extra, 'arkorder-missing-plane', 'ark.config.json', 1, 'ArkOrder planeRoots is empty; no createOrderPlane site is declared.', undefined, teethAllowed));
65
+ }
66
+ else {
67
+ const hitsByRoot = new Map();
68
+ for (const hit of input.planeRootHits) {
69
+ const list = hitsByRoot.get(hit.matchedRoot) ?? [];
70
+ list.push(hit);
71
+ hitsByRoot.set(hit.matchedRoot, list);
72
+ }
73
+ for (const pattern of roots) {
74
+ const matched = hitsByRoot.get(pattern) ?? [];
75
+ if (matched.length === 0) {
76
+ findings.push(finding(extra, 'arkorder-missing-plane', 'ark.config.json', 1, `ArkOrder plane root ${JSON.stringify(pattern)} matched no governed files and has no createOrderPlane factory.`, { target: pattern }, teethAllowed));
77
+ continue;
78
+ }
79
+ if (matched.some((hit) => hit.hasPlaneFactory))
80
+ continue;
81
+ findings.push(finding(extra, 'arkorder-missing-plane', matched[0].file, 1, `ArkOrder plane root ${JSON.stringify(pattern)} has no createOrderPlane factory.`, { target: pattern }, teethAllowed));
82
+ }
83
+ }
84
+ const prefixes = new Map(input.layers.map((layer) => [layer.name, layer.intentPrefixes ?? []]));
85
+ for (const dependency of input.dependencies) {
86
+ const specifier = dependency.specifier;
87
+ if (!specifier || !isArkOrderModuleSpecifier(specifier))
88
+ continue;
89
+ const fromLayer = input.layerForFile(dependency.from);
90
+ if (!fromLayer)
91
+ continue;
92
+ if (!isDomainRoleLayer(fromLayer, prefixes.get(fromLayer) ?? []))
93
+ continue;
94
+ findings.push(finding(extra, 'arkorder-kernel-in-domain', dependency.from, dependency.line, 'Domain-role layer imports arkgate/order; Domain stays plane-free.', { fromLayer, target: specifier }, teethAllowed));
95
+ }
96
+ for (const update of input.genericUpdates) {
97
+ findings.push(finding(extra, 'arkorder-generic-update', update.file, update.line, `Generic ${update.method}() on the order plane rewrites ξ; Haken forbids it.`, { target: update.method }, teethAllowed));
98
+ }
99
+ findings.sort((left, right) => left.file.localeCompare(right.file) ||
100
+ left.ruleId.localeCompare(right.ruleId) ||
101
+ left.line - right.line);
102
+ return { findings, completenessReasons: [] };
103
+ }
104
+ export function evaluateArkOrderEditorSensors(input) {
105
+ if (!input.arkOrder)
106
+ return [];
107
+ const planeCalls = extractArkOrderPlaneCallsFromSource(input.file, input.source);
108
+ const genericUpdates = extractArkOrderGenericUpdatesFromSource(input.file, input.source);
109
+ return evaluateArkOrderSensors({
110
+ arkOrder: input.arkOrder,
111
+ layers: [],
112
+ planeCalls,
113
+ genericUpdates,
114
+ planeRootHits: [],
115
+ dependencies: [],
116
+ layerForFile: () => input.fromLayer,
117
+ }).findings.filter((item) => item.sensor === 'arkorder-generic-update' || item.sensor === 'arkorder-kernel-in-domain');
118
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * GENERATED FILE — do not edit by hand.
3
+ *
4
+ * Canonical algorithm: src/domain/arkOrderTypes.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/ark-order-types.mjs). Zero Node I/O.
9
+ */
10
+
11
+ export const DEFAULT_MAX_XI_KEYS = 7;
@@ -111,9 +111,9 @@ function dependKinds(kind) {
111
111
  }
112
112
  function evaluateMissingRoot(extra, hits, teethAllowed) {
113
113
  const out = [];
114
- const roots = extra.compositionRoots;
114
+ const roots = extra.kernelRoots ?? extra.compositionRoots;
115
115
  if (roots.length === 0) {
116
- out.push(finding(extra, 'arkrun-missing-root', 'ark.config.json', 1, 'ArkRun compositionRoots is empty; no createArkKernel factory site is declared.', undefined, teethAllowed));
116
+ out.push(finding(extra, 'arkrun-missing-root', 'ark.config.json', 1, 'ArkRun kernelRoots is empty; no createArkKernel factory site is declared.', undefined, teethAllowed));
117
117
  return out;
118
118
  }
119
119
  const hitsByRoot = new Map();
@@ -125,14 +125,14 @@ function evaluateMissingRoot(extra, hits, teethAllowed) {
125
125
  for (const pattern of roots) {
126
126
  const matched = [...(hitsByRoot.get(pattern) ?? [])].sort((left, right) => left.file.localeCompare(right.file));
127
127
  if (matched.length === 0) {
128
- out.push(finding(extra, 'arkrun-missing-root', 'ark.config.json', 1, `ArkRun composition root ${JSON.stringify(pattern)} matched no governed files and has no createArkKernel factory.`, { target: pattern }, teethAllowed));
128
+ out.push(finding(extra, 'arkrun-missing-root', 'ark.config.json', 1, `ArkRun kernel root ${JSON.stringify(pattern)} matched no governed files and has no createArkKernel factory.`, { target: pattern }, teethAllowed));
129
129
  continue;
130
130
  }
131
131
  // Factory required in the root set, not in every glob hit.
132
132
  if (matched.some((hit) => hit.hasKernelFactory))
133
133
  continue;
134
134
  const first = matched[0];
135
- out.push(finding(extra, 'arkrun-missing-root', first.file, 1, `ArkRun composition root ${JSON.stringify(pattern)} has no createArkKernel / createStrictArkKernel factory.`, { target: pattern }, teethAllowed));
135
+ out.push(finding(extra, 'arkrun-missing-root', first.file, 1, `ArkRun kernel root ${JSON.stringify(pattern)} has no createArkKernel / createStrictArkKernel factory.`, { target: pattern }, teethAllowed));
136
136
  }
137
137
  return out;
138
138
  }
@@ -162,6 +162,13 @@ function evaluateDirectNew(extra, layers, managedNews, hits, layerForFile, teeth
162
162
  for (const constructed of managedNews) {
163
163
  if (admittedFactories.has(constructed.file))
164
164
  continue;
165
+ const name = constructed.typeName;
166
+ if (extra.ignoreDirectNewForErrors !== false && (name.endsWith('Error') || name === 'Error')) {
167
+ continue;
168
+ }
169
+ if (name.endsWith('DTO') || name.endsWith('VO')) {
170
+ continue;
171
+ }
165
172
  const fromLayer = layerForFile(constructed.file);
166
173
  if (!fromLayer || !managed.has(fromLayer))
167
174
  continue;
@@ -279,7 +286,8 @@ function fileMatchesCompositionRoot(pattern, file) {
279
286
  function compositionRootHitsForSource(extra, file, source) {
280
287
  const hasFactory = extractArkRunKernelCallsFromSource(file, source).some((call) => call.kind === 'factory');
281
288
  const hits = [];
282
- for (const pattern of extra.compositionRoots) {
289
+ const roots = extra.kernelRoots ?? extra.compositionRoots;
290
+ for (const pattern of roots) {
283
291
  if (!fileMatchesCompositionRoot(pattern, file))
284
292
  continue;
285
293
  hits.push({ file, matchedRoot: pattern, hasKernelFactory: hasFactory });
@@ -8,8 +8,9 @@
8
8
  * Pure CLI helper (bin/lib/config-contract.mjs). Zero Node I/O.
9
9
  */
10
10
 
11
- /** Current published ark.config.json schema version (ADR 0020: 1.2 adds optional arkRun). */
12
- export const ARK_CONFIG_SCHEMA_VERSION = '1.2';
11
+ import { ARK_ORDER_SCHEMA_DEF, ARK_RUN_SCHEMA_DEF, defaultedArkOrder, defaultedArkRun, validateArkOrderExtra, validateArkRunExtra, } from './config-extras.mjs';
12
+ /** Current published ark.config.json schema version (ADR 0027: 1.3 adds optional arkOrder). */
13
+ export const ARK_CONFIG_SCHEMA_VERSION = '1.3';
13
14
  export const ARK_CONFIG_SCHEMA_URL = 'https://unpkg.com/arkgate@2/schemas/ark.config.schema.json';
14
15
  const DEFAULT_LAYER_NAMES = [
15
16
  'DomainModel',
@@ -51,6 +52,7 @@ export const ARK_CONFIG_MIGRATIONS = [
51
52
  { from: 'unversioned', to: '1.0' },
52
53
  { from: '1.0', to: '1.1' },
53
54
  { from: '1.1', to: '1.2' },
55
+ { from: '1.2', to: '1.3' },
54
56
  ];
55
57
  const stringArraySchema = {
56
58
  type: 'array',
@@ -82,11 +84,7 @@ export const ARK_CONFIG_SCHEMA = {
82
84
  exclude: { ...stringArraySchema, default: [] },
83
85
  excludeGenerated: { type: 'boolean', default: true },
84
86
  frameworkOverlay: { type: 'string', minLength: 1 },
85
- layers: {
86
- type: 'array',
87
- default: [],
88
- items: { $ref: '#/$defs/layer' },
89
- },
87
+ layers: { type: 'array', default: [], items: { $ref: '#/$defs/layer' } },
90
88
  rules: {
91
89
  type: 'array',
92
90
  default: DEFAULT_ARK_CONFIG_RULES,
@@ -115,6 +113,8 @@ export const ARK_CONFIG_SCHEMA = {
115
113
  },
116
114
  /** ADR 0020 — optional ArkRun extra. Absence is silent; unknown keys fail closed. */
117
115
  arkRun: { $ref: '#/$defs/arkRun' },
116
+ /** ADR 0027 — optional ArkOrder extra. Absence is silent; unknown keys fail closed. */
117
+ arkOrder: { $ref: '#/$defs/arkOrder' },
118
118
  /** Team parliament — GitHub handles or emails who may loosen the law (not part of policy hash). */
119
119
  stewards: { ...stringArraySchema, default: [] },
120
120
  },
@@ -184,20 +184,8 @@ export const ARK_CONFIG_SCHEMA = {
184
184
  allowDisabledPeerIsolation: { type: 'boolean', default: false },
185
185
  },
186
186
  },
187
- arkRun: {
188
- type: 'object',
189
- additionalProperties: false,
190
- properties: {
191
- mode: {
192
- type: 'string',
193
- enum: ['advisory', 'enforced'],
194
- default: 'advisory',
195
- },
196
- compositionRoots: { ...stringArraySchema, default: [] },
197
- managedLayers: { ...stringArraySchema, default: [] },
198
- requireDeclarations: { type: 'boolean', default: true },
199
- },
200
- },
187
+ arkRun: ARK_RUN_SCHEMA_DEF,
188
+ arkOrder: ARK_ORDER_SCHEMA_DEF,
201
189
  },
202
190
  };
203
191
  export class ArkConfigValidationError extends Error {
@@ -331,17 +319,6 @@ function validateNode(value, schema, path, root, issues) {
331
319
  }
332
320
  }
333
321
  }
334
- function defaultedArkRun(value) {
335
- if (!isObject(value))
336
- return value;
337
- return {
338
- ...value,
339
- mode: value.mode === undefined ? 'advisory' : value.mode,
340
- compositionRoots: value.compositionRoots === undefined ? [] : value.compositionRoots,
341
- managedLayers: value.managedLayers === undefined ? [] : value.managedLayers,
342
- requireDeclarations: value.requireDeclarations === undefined ? true : value.requireDeclarations,
343
- };
344
- }
345
322
  function defaultedConfig(input) {
346
323
  const result = {
347
324
  ...input,
@@ -355,54 +332,20 @@ function defaultedConfig(input) {
355
332
  };
356
333
  if (input.arkRun !== undefined)
357
334
  result.arkRun = defaultedArkRun(input.arkRun);
335
+ if (input.arkOrder !== undefined)
336
+ result.arkOrder = defaultedArkOrder(input.arkOrder);
358
337
  return result;
359
338
  }
360
- function validateArkRunExtra(config, issues) {
361
- const extra = config.arkRun;
362
- if (extra === undefined || !isObject(extra))
363
- return;
364
- const layerNames = new Set();
365
- if (Array.isArray(config.layers)) {
366
- for (const layer of config.layers) {
367
- if (isObject(layer) && typeof layer.name === 'string' && layer.name.length > 0) {
368
- layerNames.add(layer.name);
369
- }
370
- }
371
- }
372
- const managed = extra.managedLayers;
373
- if (Array.isArray(managed)) {
374
- managed.forEach((name, index) => {
375
- if (typeof name === 'string' && name.length > 0 && !layerNames.has(name)) {
376
- issues.push({
377
- path: `$.arkRun.managedLayers[${index}]`,
378
- message: `layer ${JSON.stringify(name)} is not declared in layers[]`,
379
- });
380
- }
381
- });
382
- }
383
- if (extra.mode === 'enforced') {
384
- const roots = extra.compositionRoots;
385
- if (!Array.isArray(roots) || roots.length === 0) {
386
- issues.push({
387
- path: '$.arkRun.compositionRoots',
388
- message: 'ARKRUN_MISSING_ROOT: enforced mode requires at least one composition root',
389
- });
390
- }
391
- if (!Array.isArray(managed) || managed.length === 0) {
392
- issues.push({
393
- path: '$.arkRun.managedLayers',
394
- message: 'enforced mode requires at least one managed layer',
395
- });
396
- }
397
- }
398
- }
399
339
  function migratedFromOf(originalVersion) {
400
340
  if (originalVersion === ARK_CONFIG_SCHEMA_VERSION)
401
341
  return null;
402
342
  if (originalVersion === 'unversioned')
403
343
  return 'unversioned';
404
- if (originalVersion === '1.0' || originalVersion === '1.1')
344
+ if (originalVersion === '1.0' ||
345
+ originalVersion === '1.1' ||
346
+ originalVersion === '1.2') {
405
347
  return originalVersion;
348
+ }
406
349
  return null;
407
350
  }
408
351
  function knownInputVersions() {
@@ -480,6 +423,7 @@ export function loadArkConfigContract(input, source = 'ark.config.json') {
480
423
  const issues = [];
481
424
  validateNode(candidate, ARK_CONFIG_SCHEMA, '$', ARK_CONFIG_SCHEMA, issues);
482
425
  validateArkRunExtra(candidate, issues);
426
+ validateArkOrderExtra(candidate, issues);
483
427
  if (issues.length > 0)
484
428
  throw new ArkConfigValidationError(source, issues);
485
429
  return { config: candidate, migratedFrom };