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,13 +9,15 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { ARK_ORDER_SCHEMA_DEF, ARK_RUN_SCHEMA_DEF, defaultedArkOrder, defaultedArkRun, validateArkOrderExtra, validateArkRunExtra, } from './config-extras.mjs';
|
|
12
|
+
import { CHILD_SLICES_SCHEMA_DEF, SHARED_IMPORTS_SLICE_SCHEMA_DEF, validateSliceContract, } from './config-contract-slices.mjs';
|
|
13
|
+
import { isObject, propertyPath, validateSchemaNode, valueType } from './schema-validation.mjs';
|
|
12
14
|
import { canonicalStewardId } from './team-parliament.mjs';
|
|
13
15
|
/** Current published ark.config.json schema version (ADR 0027: 1.3 adds optional arkOrder). */
|
|
14
16
|
export const ARK_CONFIG_SCHEMA_VERSION = '1.3';
|
|
15
17
|
/** Closed layer trust tags. Optional; absence is silent. Not a schemaVersion bump. */
|
|
16
18
|
export const LAYER_TRUST_BOUNDARIES = ['public', 'auth', 'admin', 'internal'];
|
|
17
19
|
/** Future house: empty globs are expected; missing owners stay silent even when required. */
|
|
18
|
-
|
|
20
|
+
function isFutureHouseLayer(layer) {
|
|
19
21
|
return layer?.optional === true || layer?.reserved === true || layer?.allowEmpty === true;
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
@@ -140,10 +142,21 @@ export const ARK_CONFIG_SCHEMA = {
|
|
|
140
142
|
* maxFiles also bounds structural-hint preload (orchestration-only / thin-adapter /
|
|
141
143
|
* writes-via-aggregate). There is no arkrules.hintBudget. */
|
|
142
144
|
coverage: { $ref: '#/$defs/coverage' },
|
|
143
|
-
/** ADR 0012 — layer name →
|
|
145
|
+
/** ADR 0012 — layer name → one path, or a list of paths merged into that layer. */
|
|
144
146
|
arkRules: {
|
|
145
147
|
type: 'object',
|
|
146
|
-
additionalProperties: {
|
|
148
|
+
additionalProperties: {
|
|
149
|
+
description: 'A project-relative ArkRules path, or a list of paths merged into one catalog for this layer. A string keeps today\'s behaviour. An older build that types this value as a string rejects an array at config load.',
|
|
150
|
+
oneOf: [
|
|
151
|
+
{ type: 'string', minLength: 1 },
|
|
152
|
+
{
|
|
153
|
+
type: 'array',
|
|
154
|
+
minItems: 1,
|
|
155
|
+
uniqueItems: true,
|
|
156
|
+
items: { type: 'string', minLength: 1 },
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
},
|
|
147
160
|
default: {},
|
|
148
161
|
},
|
|
149
162
|
/** ADR 0020 — optional ArkRun extra. Absence is silent; unknown keys fail closed. */
|
|
@@ -217,7 +230,7 @@ export const ARK_CONFIG_SCHEMA = {
|
|
|
217
230
|
sliceIdentity: {
|
|
218
231
|
type: 'string',
|
|
219
232
|
enum: ['path', 'stars'],
|
|
220
|
-
description: 'How a starred sliceFolders prefix is named. Absent and path keep today\'s ids (every literal and star binding). stars keeps the last literal plus
|
|
233
|
+
description: 'How a starred sliceFolders prefix is named. Absent and path keep today\'s ids (every literal and star binding). stars keeps the last literal plus every star binding (a star before the last literal is kept too), so parallel trees share one feature id. Bare names are unchanged.',
|
|
221
234
|
},
|
|
222
235
|
sharedRoots: { ...stringArraySchema, minItems: 1 },
|
|
223
236
|
allowedCrossSlice: {
|
|
@@ -233,69 +246,8 @@ export const ARK_CONFIG_SCHEMA = {
|
|
|
233
246
|
},
|
|
234
247
|
},
|
|
235
248
|
},
|
|
236
|
-
sharedImportsSlice:
|
|
237
|
-
|
|
238
|
-
enum: ['deny', 'deny-cross-parent'],
|
|
239
|
-
description: 'deny blocks every shared-root import of a slice. deny-cross-parent leaves that hop as a warning and, in ark-check and CI only, reports a slice that reaches another universe through a shared root. The write hook and ESLint see one edge and do not block it. arkgate 4.8.22 and older reject deny-cross-parent.',
|
|
240
|
-
},
|
|
241
|
-
childSlices: {
|
|
242
|
-
type: 'object',
|
|
243
|
-
additionalProperties: false,
|
|
244
|
-
required: ['sliceFolders'],
|
|
245
|
-
description: 'Optional inner wall under this rule. Absent keeps today\'s universe wall. sliceFolders names children. commonFolders and flat files are universe common. siblings is "deny" (default), "advisory", or { default, enforce }. parentMayImportChild defaults to false. allowedCrossSlice may use a whole-segment *. It clears only a sibling crossing. Cross-parent has no advisory knob. arkgate 4.8.22 and older reject this key. A build that still rejects unknown childSlices fields rejects allowedCrossSlice at config load. A build that still types siblings as a string enum rejects the object at config load.',
|
|
246
|
-
properties: {
|
|
247
|
-
sliceFolders: { ...stringArraySchema, minItems: 1 },
|
|
248
|
-
sliceIdentity: {
|
|
249
|
-
type: 'string',
|
|
250
|
-
enum: ['path', 'stars'],
|
|
251
|
-
},
|
|
252
|
-
commonFolders: { ...stringArraySchema, minItems: 1 },
|
|
253
|
-
siblings: {
|
|
254
|
-
description: '"deny" or "advisory", or { default, enforce } so listed subtrees are errors while the default stays advisory. A string-enum build rejects the object at config load (must be one of deny, advisory).',
|
|
255
|
-
oneOf: [
|
|
256
|
-
{ type: 'string', enum: ['deny', 'advisory'] },
|
|
257
|
-
{
|
|
258
|
-
type: 'object',
|
|
259
|
-
additionalProperties: false,
|
|
260
|
-
required: ['default'],
|
|
261
|
-
properties: {
|
|
262
|
-
default: { type: 'string', enum: ['deny', 'advisory'] },
|
|
263
|
-
enforce: { ...stringArraySchema },
|
|
264
|
-
},
|
|
265
|
-
},
|
|
266
|
-
],
|
|
267
|
-
},
|
|
268
|
-
parentMayImportChild: { type: 'boolean' },
|
|
269
|
-
sliceAliases: {
|
|
270
|
-
type: 'array',
|
|
271
|
-
minItems: 1,
|
|
272
|
-
description: 'Maps a source path glob onto a child slice id (universe id plus one child segment) so files outside the slice trees take that universe and child for both walls. A bare name, a universe id alone, an unknown universe, and a wildcard in to are rejected. The glob may not overlap a slice folder, and two aliases may not match the same file. Doctor lists each alias as an owed move. A build that rejects unknown childSlices fields fails at config load (unknown field). arkgate 4.8.22 and older reject childSlices.',
|
|
273
|
-
items: {
|
|
274
|
-
type: 'object',
|
|
275
|
-
additionalProperties: false,
|
|
276
|
-
required: ['from', 'to'],
|
|
277
|
-
properties: {
|
|
278
|
-
from: { type: 'string', minLength: 1 },
|
|
279
|
-
to: { type: 'string', minLength: 1 },
|
|
280
|
-
},
|
|
281
|
-
},
|
|
282
|
-
},
|
|
283
|
-
allowedCrossSlice: {
|
|
284
|
-
type: 'array',
|
|
285
|
-
minItems: 1,
|
|
286
|
-
description: 'Directed child-slice allowances. * matches one whole path segment (features/projects/*). ** and a partial segment are rejected. A bare name is rejected. This list clears only CROSS_SIBLING_SLICE. It cannot clear CROSS_PARENT_SLICE or CROSS_PARENT_VIA_SHARED. The universe allowedCrossSlice still treats * as a literal. A build that rejects unknown childSlices fields fails at config load (unknown field).',
|
|
287
|
-
items: {
|
|
288
|
-
type: 'object',
|
|
289
|
-
additionalProperties: false,
|
|
290
|
-
required: ['from', 'to'],
|
|
291
|
-
properties: {
|
|
292
|
-
from: { type: 'string', minLength: 1 },
|
|
293
|
-
to: { type: 'string', minLength: 1 },
|
|
294
|
-
},
|
|
295
|
-
},
|
|
296
|
-
},
|
|
297
|
-
},
|
|
298
|
-
},
|
|
249
|
+
sharedImportsSlice: SHARED_IMPORTS_SLICE_SCHEMA_DEF,
|
|
250
|
+
childSlices: CHILD_SLICES_SCHEMA_DEF,
|
|
299
251
|
},
|
|
300
252
|
},
|
|
301
253
|
safety: {
|
|
@@ -338,538 +290,34 @@ export class ArkConfigValidationError extends Error {
|
|
|
338
290
|
this.issues = issues;
|
|
339
291
|
}
|
|
340
292
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
|
|
346
|
-
? `${parent}.${key}`
|
|
347
|
-
: `${parent}[${JSON.stringify(key)}]`;
|
|
348
|
-
}
|
|
349
|
-
function valueType(value) {
|
|
350
|
-
if (value === null)
|
|
351
|
-
return 'null';
|
|
352
|
-
if (Array.isArray(value))
|
|
353
|
-
return 'array';
|
|
354
|
-
return typeof value;
|
|
355
|
-
}
|
|
356
|
-
function resolveSchemaRef(ref, root) {
|
|
357
|
-
const prefix = '#/$defs/';
|
|
358
|
-
if (!ref.startsWith(prefix))
|
|
359
|
-
return undefined;
|
|
360
|
-
return root.$defs[ref.slice(prefix.length)];
|
|
361
|
-
}
|
|
362
|
-
function validateNode(value, schema, path, root, issues) {
|
|
363
|
-
if (schema.$ref) {
|
|
364
|
-
const referenced = resolveSchemaRef(schema.$ref, root);
|
|
365
|
-
if (!referenced) {
|
|
366
|
-
issues.push({ path, message: `schema reference ${schema.$ref} cannot be resolved` });
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
validateNode(value, referenced, path, root, issues);
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
if (schema.const !== undefined && !Object.is(value, schema.const)) {
|
|
373
|
-
issues.push({ path, message: `must equal ${JSON.stringify(schema.const)}` });
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
if (schema.enum && !schema.enum.some((candidate) => Object.is(candidate, value))) {
|
|
377
|
-
issues.push({ path, message: `must be one of ${schema.enum.map(String).join(', ')}` });
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
if (schema.type === 'object') {
|
|
381
|
-
if (!isObject(value)) {
|
|
382
|
-
issues.push({ path, message: `must be an object; received ${valueType(value)}` });
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
const properties = schema.properties ?? {};
|
|
386
|
-
for (const key of schema.required ?? []) {
|
|
387
|
-
if (value[key] === undefined) {
|
|
388
|
-
issues.push({ path: propertyPath(path, key), message: 'is required' });
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
if (schema.additionalProperties === false) {
|
|
392
|
-
for (const key of Object.keys(value)) {
|
|
393
|
-
if (!(key in properties)) {
|
|
394
|
-
issues.push({ path: propertyPath(path, key), message: 'unknown field' });
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
else if (schema.additionalProperties !== undefined &&
|
|
399
|
-
schema.additionalProperties !== true &&
|
|
400
|
-
typeof schema.additionalProperties === 'object') {
|
|
401
|
-
const additional = schema.additionalProperties;
|
|
402
|
-
for (const key of Object.keys(value)) {
|
|
403
|
-
if (!(key in properties)) {
|
|
404
|
-
validateNode(value[key], additional, propertyPath(path, key), root, issues);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
for (const [key, childSchema] of Object.entries(properties)) {
|
|
409
|
-
if (value[key] !== undefined) {
|
|
410
|
-
validateNode(value[key], childSchema, propertyPath(path, key), root, issues);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
if (schema.type === 'array') {
|
|
416
|
-
if (!Array.isArray(value)) {
|
|
417
|
-
issues.push({ path, message: `must be an array; received ${valueType(value)}` });
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
421
|
-
issues.push({ path, message: `must contain at least ${schema.minItems} item(s)` });
|
|
422
|
-
}
|
|
423
|
-
if (schema.uniqueItems) {
|
|
424
|
-
const serialized = value.map((entry) => JSON.stringify(entry));
|
|
425
|
-
if (new Set(serialized).size !== serialized.length) {
|
|
426
|
-
issues.push({ path, message: 'must not contain duplicate items' });
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
if (schema.items) {
|
|
430
|
-
value.forEach((entry, index) => validateNode(entry, schema.items, `${path}[${index}]`, root, issues));
|
|
431
|
-
}
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
if (schema.type === 'string') {
|
|
435
|
-
if (typeof value !== 'string') {
|
|
436
|
-
issues.push({ path, message: `must be a string; received ${valueType(value)}` });
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
440
|
-
issues.push({ path, message: `must contain at least ${schema.minLength} character(s)` });
|
|
441
|
-
}
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
if (schema.type === 'boolean') {
|
|
445
|
-
if (typeof value !== 'boolean') {
|
|
446
|
-
issues.push({ path, message: `must be a boolean; received ${valueType(value)}` });
|
|
447
|
-
}
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
if (schema.type === 'integer') {
|
|
451
|
-
if (!Number.isInteger(value)) {
|
|
452
|
-
issues.push({ path, message: `must be an integer; received ${valueType(value)}` });
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
456
|
-
issues.push({ path, message: `must be at least ${schema.minimum}` });
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
const SIBLING_MODES = ['deny', 'advisory'];
|
|
461
|
-
/**
|
|
462
|
-
* String enum builds reject this object before they reach a decision.
|
|
463
|
-
* The message names both forms so a bad value is obvious at config load.
|
|
464
|
-
*/
|
|
465
|
-
const SIBLINGS_FORM_MESSAGE = 'must be "deny", "advisory", or { "default": "deny" | "advisory", "enforce": ["<child id or subtree path>"] }';
|
|
466
|
-
function validateChildSliceSiblings(candidate, issues) {
|
|
467
|
-
const rules = candidate.rules;
|
|
468
|
-
if (!Array.isArray(rules))
|
|
469
|
-
return;
|
|
470
|
-
rules.forEach((rule, index) => {
|
|
471
|
-
if (!isObject(rule))
|
|
472
|
-
return;
|
|
473
|
-
const child = rule.childSlices;
|
|
474
|
-
if (!isObject(child) || child.siblings === undefined)
|
|
475
|
-
return;
|
|
476
|
-
const siblings = child.siblings;
|
|
477
|
-
const path = `$.rules[${index}].childSlices.siblings`;
|
|
478
|
-
if (typeof siblings === 'string') {
|
|
479
|
-
if (!SIBLING_MODES.includes(siblings)) {
|
|
480
|
-
issues.push({ path, message: SIBLINGS_FORM_MESSAGE });
|
|
481
|
-
}
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
if (!isObject(siblings)) {
|
|
485
|
-
issues.push({ path, message: SIBLINGS_FORM_MESSAGE });
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
for (const key of Object.keys(siblings)) {
|
|
489
|
-
if (key !== 'default' && key !== 'enforce') {
|
|
490
|
-
issues.push({ path: propertyPath(path, key), message: 'unknown field' });
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
if (siblings.default === undefined) {
|
|
494
|
-
issues.push({ path: `${path}.default`, message: 'is required' });
|
|
495
|
-
}
|
|
496
|
-
else if (siblings.default !== 'deny' && siblings.default !== 'advisory') {
|
|
497
|
-
issues.push({ path: `${path}.default`, message: 'must be deny or advisory' });
|
|
498
|
-
}
|
|
499
|
-
if (siblings.enforce === undefined)
|
|
500
|
-
return;
|
|
501
|
-
if (!Array.isArray(siblings.enforce)) {
|
|
502
|
-
issues.push({
|
|
503
|
-
path: `${path}.enforce`,
|
|
504
|
-
message: 'must be an array of child slice ids or subtree paths',
|
|
505
|
-
});
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
const seen = new Set();
|
|
509
|
-
siblings.enforce.forEach((entry, entryIndex) => {
|
|
510
|
-
const entryPath = `${path}.enforce[${entryIndex}]`;
|
|
511
|
-
if (typeof entry !== 'string' || entry.trim().length === 0) {
|
|
512
|
-
issues.push({ path: entryPath, message: 'must be a non-empty child slice id or subtree path' });
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
const normalized = trimTrailingSlashes(entry.trim().replace(/\\/g, '/'));
|
|
516
|
-
if (normalized.split('/').some((part) => part.length === 0 || part === '.' || part === '..')) {
|
|
517
|
-
issues.push({ path: entryPath, message: 'must be a child slice id or subtree path without . or ..' });
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
if (normalized.includes('*')) {
|
|
521
|
-
issues.push({
|
|
522
|
-
path: entryPath,
|
|
523
|
-
message: 'must be a child slice id or subtree path. * is not a wildcard on siblings.enforce',
|
|
524
|
-
});
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
if (seen.has(normalized)) {
|
|
528
|
-
issues.push({ path: entryPath, message: 'duplicate enforce entry' });
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
seen.add(normalized);
|
|
532
|
-
});
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
const BARE_CHILD_CROSS_SLICE = 'must be a slice id with a slash. A bare name is ambiguous across universes.';
|
|
536
|
-
const WHOLE_SEGMENT_STAR = '* matches one whole path segment. ** and a partial segment are not wildcards.';
|
|
537
|
-
/**
|
|
538
|
-
* Whole-segment `*` is legal only on this list. A bare name is rejected.
|
|
539
|
-
* The universe allowedCrossSlice is not checked here and still treats `*` as a literal.
|
|
540
|
-
*/
|
|
541
|
-
function childCrossSlicePatternIssue(raw) {
|
|
542
|
-
const trimmed = trimTrailingSlashes(raw.trim().replace(/\\/g, '/'));
|
|
543
|
-
if (trimmed.length === 0)
|
|
544
|
-
return 'must be a non-empty slice id';
|
|
545
|
-
if (!trimmed.includes('/'))
|
|
546
|
-
return BARE_CHILD_CROSS_SLICE;
|
|
547
|
-
const parts = trimmed.split('/');
|
|
548
|
-
for (const part of parts) {
|
|
549
|
-
if (part.length === 0 || part === '.' || part === '..') {
|
|
550
|
-
return 'must be a slice id without empty, . or .. segments';
|
|
551
|
-
}
|
|
552
|
-
if (part.includes('*') && part !== '*')
|
|
553
|
-
return WHOLE_SEGMENT_STAR;
|
|
554
|
-
}
|
|
555
|
-
return null;
|
|
556
|
-
}
|
|
557
|
-
function childCrossSlicePatternKey(raw) {
|
|
558
|
-
return trimTrailingSlashes(raw.trim().replace(/\\/g, '/')).toLowerCase();
|
|
559
|
-
}
|
|
560
|
-
function validateChildSliceAllowedCrossSlice(candidate, issues) {
|
|
561
|
-
const rules = candidate.rules;
|
|
562
|
-
if (!Array.isArray(rules))
|
|
293
|
+
/** The schema walk has no oneOf, so string-or-array `arkRules` values are checked here. */
|
|
294
|
+
function validateArkRulesRefs(candidate, issues) {
|
|
295
|
+
const refs = candidate.arkRules;
|
|
296
|
+
if (refs === undefined || !isObject(refs))
|
|
563
297
|
return;
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
return;
|
|
570
|
-
const edges = child.allowedCrossSlice;
|
|
571
|
-
const path = `$.rules[${index}].childSlices.allowedCrossSlice`;
|
|
572
|
-
if (!Array.isArray(edges)) {
|
|
573
|
-
issues.push({ path, message: 'must be an array of { from, to } child slice ids' });
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
const seen = new Set();
|
|
577
|
-
edges.forEach((edge, edgeIndex) => {
|
|
578
|
-
const edgePath = `${path}[${edgeIndex}]`;
|
|
579
|
-
if (!isObject(edge)) {
|
|
580
|
-
issues.push({ path: edgePath, message: 'must be an object with from and to' });
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
for (const key of Object.keys(edge)) {
|
|
584
|
-
if (key !== 'from' && key !== 'to') {
|
|
585
|
-
issues.push({ path: propertyPath(edgePath, key), message: 'unknown field' });
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
for (const side of ['from', 'to']) {
|
|
589
|
-
const value = edge[side];
|
|
590
|
-
const sidePath = `${edgePath}.${side}`;
|
|
591
|
-
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
592
|
-
issues.push({ path: sidePath, message: 'must be a non-empty slice id' });
|
|
593
|
-
continue;
|
|
594
|
-
}
|
|
595
|
-
const issue = childCrossSlicePatternIssue(value);
|
|
596
|
-
if (issue)
|
|
597
|
-
issues.push({ path: sidePath, message: issue });
|
|
598
|
-
}
|
|
599
|
-
if (typeof edge.from !== 'string' || typeof edge.to !== 'string')
|
|
600
|
-
return;
|
|
601
|
-
const key = `${childCrossSlicePatternKey(edge.from)}\0${childCrossSlicePatternKey(edge.to)}`;
|
|
602
|
-
if (!key.startsWith('\0') && !key.endsWith('\0')) {
|
|
603
|
-
if (seen.has(key)) {
|
|
604
|
-
issues.push({ path: edgePath, message: 'duplicate child slice allowance' });
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
seen.add(key);
|
|
608
|
-
}
|
|
609
|
-
});
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
const ALIAS_TARGET_MESSAGE = 'must be a child of an existing universe (the universe id this rule already names, plus one child segment). A bare name, a universe id alone, an unknown universe, and a wildcard are rejected.';
|
|
613
|
-
const ALIAS_OVERLAP_MESSAGE = 'overlaps a slice folder. An alias covers only files outside the slice trees.';
|
|
614
|
-
const ALIAS_SAME_FILE_MESSAGE = 'two slice aliases match the same file';
|
|
615
|
-
function aliasPathSegments(raw) {
|
|
616
|
-
return trimTrailingSlashes(raw.trim().replace(/\\/g, '/')).toLowerCase().split('/').filter((part) => part.length > 0);
|
|
617
|
-
}
|
|
618
|
-
function concreteGlobPrefix(segments) {
|
|
619
|
-
const prefix = [];
|
|
620
|
-
for (const segment of segments) {
|
|
621
|
-
if (segment.includes('*'))
|
|
622
|
-
break;
|
|
623
|
-
prefix.push(segment);
|
|
624
|
-
}
|
|
625
|
-
return prefix;
|
|
626
|
-
}
|
|
627
|
-
function segmentsArePrefix(prefix, full) {
|
|
628
|
-
if (prefix.length > full.length)
|
|
629
|
-
return false;
|
|
630
|
-
for (let index = 0; index < prefix.length; index += 1) {
|
|
631
|
-
if (prefix[index] !== full[index])
|
|
632
|
-
return false;
|
|
633
|
-
}
|
|
634
|
-
return true;
|
|
635
|
-
}
|
|
636
|
-
function anchoredSlicePrefix(entry) {
|
|
637
|
-
const segments = entry.split(/[/\\]/).filter(Boolean).map((part) => part.toLowerCase());
|
|
638
|
-
if (segments.length < 2 || segments[0] === '*' || segments[0] === '**')
|
|
639
|
-
return null;
|
|
640
|
-
if (!segments.some((part) => part === '*'))
|
|
641
|
-
return null;
|
|
642
|
-
const prefix = [];
|
|
643
|
-
for (const segment of segments) {
|
|
644
|
-
if (segment === '*' || segment === '**')
|
|
645
|
-
break;
|
|
646
|
-
if (segment.includes('*'))
|
|
647
|
-
return null;
|
|
648
|
-
prefix.push(segment);
|
|
649
|
-
}
|
|
650
|
-
return prefix.length > 0 ? prefix : null;
|
|
651
|
-
}
|
|
652
|
-
/** Trailing ** does not invent a slice-folder name the glob does not already reach. */
|
|
653
|
-
function globOverlapsAnchored(glob, patternPrefix) {
|
|
654
|
-
const prefix = concreteGlobPrefix(glob);
|
|
655
|
-
const wild = prefix.length < glob.length;
|
|
656
|
-
const rooted = prefix[0] === 'src' || prefix[0] === 'app' ? prefix.slice(1) : prefix;
|
|
657
|
-
if (segmentsArePrefix(patternPrefix, prefix) || segmentsArePrefix(patternPrefix, rooted))
|
|
658
|
-
return true;
|
|
659
|
-
if (!wild)
|
|
660
|
-
return false;
|
|
661
|
-
return ((segmentsArePrefix(prefix, patternPrefix) && prefix.length < patternPrefix.length) ||
|
|
662
|
-
(segmentsArePrefix(prefix, ['src', ...patternPrefix]) && prefix.length < patternPrefix.length + 1) ||
|
|
663
|
-
(segmentsArePrefix(prefix, ['app', ...patternPrefix]) && prefix.length < patternPrefix.length + 1));
|
|
664
|
-
}
|
|
665
|
-
function globOverlapsBare(glob, name) {
|
|
666
|
-
for (let index = 0; index < glob.length; index += 1) {
|
|
667
|
-
if (glob[index] !== name && glob[index] !== '*')
|
|
298
|
+
for (const [layer, value] of Object.entries(refs)) {
|
|
299
|
+
const path = propertyPath('$.arkRules', layer);
|
|
300
|
+
if (typeof value === 'string') {
|
|
301
|
+
if (value.length === 0)
|
|
302
|
+
issues.push({ path, message: 'must be a non-empty relative path string' });
|
|
668
303
|
continue;
|
|
669
|
-
const rest = glob.slice(index + 1);
|
|
670
|
-
if (rest.length >= 2)
|
|
671
|
-
return true;
|
|
672
|
-
if (rest.some((segment) => segment === '*' || segment === '**'))
|
|
673
|
-
return true;
|
|
674
|
-
}
|
|
675
|
-
const prefix = concreteGlobPrefix(glob);
|
|
676
|
-
return (prefix.length < glob.length &&
|
|
677
|
-
(prefix.length === 0 || (prefix.length === 1 && (prefix[0] === 'src' || prefix[0] === 'app'))));
|
|
678
|
-
}
|
|
679
|
-
function globOverlapsSliceEntry(glob, entry) {
|
|
680
|
-
const segments = entry.split(/[/\\]/).filter(Boolean).map((part) => part.toLowerCase());
|
|
681
|
-
if (segments.length === 1 && segments[0] && !segments[0].includes('*')) {
|
|
682
|
-
return globOverlapsBare(glob, segments[0]);
|
|
683
|
-
}
|
|
684
|
-
const prefix = anchoredSlicePrefix(entry);
|
|
685
|
-
return prefix ? globOverlapsAnchored(glob, prefix) : false;
|
|
686
|
-
}
|
|
687
|
-
function aliasGlobIssue(raw) {
|
|
688
|
-
const trimmed = trimTrailingSlashes(raw.trim().replace(/\\/g, '/'));
|
|
689
|
-
if (trimmed.length === 0)
|
|
690
|
-
return 'must be a non-empty path glob';
|
|
691
|
-
if (trimmed === '*' || trimmed === '**')
|
|
692
|
-
return 'must not cover the whole tree';
|
|
693
|
-
for (const part of trimmed.split('/')) {
|
|
694
|
-
if (part.length === 0 || part === '.' || part === '..') {
|
|
695
|
-
return 'must be a path glob without empty, . or .. segments';
|
|
696
304
|
}
|
|
697
|
-
if (
|
|
305
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
306
|
+
issues.push({ path, message: 'must be a non-empty path string or an array of paths' });
|
|
698
307
|
continue;
|
|
699
|
-
if (part.includes('*') || part.includes('?') || part.includes('{') || part.includes('[')) {
|
|
700
|
-
return '* matches one whole path segment. ** is a whole segment. A partial segment is not a wildcard.';
|
|
701
308
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
if (!Array.isArray(folders))
|
|
708
|
-
return shapes;
|
|
709
|
-
const stars = identity === 'stars';
|
|
710
|
-
for (const raw of folders) {
|
|
711
|
-
if (typeof raw !== 'string' || raw.length === 0)
|
|
712
|
-
continue;
|
|
713
|
-
const segments = raw.split(/[/\\]/).filter(Boolean).map((part) => part.toLowerCase());
|
|
714
|
-
if (segments.length === 1 && segments[0] && !segments[0].includes('*')) {
|
|
715
|
-
shapes.push({ literals: [segments[0], null] });
|
|
716
|
-
continue;
|
|
717
|
-
}
|
|
718
|
-
if (segments.some((part) => part === '**' || (part.includes('*') && part !== '*')))
|
|
719
|
-
continue;
|
|
720
|
-
if (!segments.includes('*'))
|
|
721
|
-
continue;
|
|
722
|
-
if (stars) {
|
|
723
|
-
let lastLiteral = -1;
|
|
724
|
-
for (let index = 0; index < segments.length; index += 1) {
|
|
725
|
-
if (segments[index] !== '*')
|
|
726
|
-
lastLiteral = index;
|
|
727
|
-
}
|
|
728
|
-
if (lastLiteral < 0)
|
|
729
|
-
continue;
|
|
730
|
-
shapes.push({
|
|
731
|
-
literals: segments.slice(lastLiteral).map((part) => (part === '*' ? null : part)),
|
|
732
|
-
});
|
|
733
|
-
continue;
|
|
734
|
-
}
|
|
735
|
-
shapes.push({ literals: segments.map((part) => (part === '*' ? null : part)) });
|
|
736
|
-
}
|
|
737
|
-
return shapes;
|
|
738
|
-
}
|
|
739
|
-
function shapeMatches(shape, segments) {
|
|
740
|
-
if (shape.literals.length !== segments.length)
|
|
741
|
-
return false;
|
|
742
|
-
return shape.literals.every((literal, index) => literal === null || literal === segments[index]);
|
|
743
|
-
}
|
|
744
|
-
function aliasTargetIssue(raw, shapes) {
|
|
745
|
-
const trimmed = trimTrailingSlashes(raw.trim().replace(/\\/g, '/'));
|
|
746
|
-
if (trimmed.length === 0 || trimmed.split('/').some((part) => part.length === 0 || part === '.' || part === '..')) {
|
|
747
|
-
return ALIAS_TARGET_MESSAGE;
|
|
748
|
-
}
|
|
749
|
-
if (trimmed.includes('*') || !trimmed.includes('/'))
|
|
750
|
-
return ALIAS_TARGET_MESSAGE;
|
|
751
|
-
const parts = trimmed.split('/').map((part) => part.toLowerCase());
|
|
752
|
-
if (shapes.length === 0)
|
|
753
|
-
return ALIAS_TARGET_MESSAGE;
|
|
754
|
-
if (shapes.some((shape) => shapeMatches(shape, parts)))
|
|
755
|
-
return ALIAS_TARGET_MESSAGE;
|
|
756
|
-
if (shapes.some((shape) => shapeMatches(shape, parts.slice(0, -1))))
|
|
757
|
-
return null;
|
|
758
|
-
return ALIAS_TARGET_MESSAGE;
|
|
759
|
-
}
|
|
760
|
-
/** A leading src/ or app/ is optional at match time, so both spellings are one glob. */
|
|
761
|
-
function aliasGlobVariants(segments) {
|
|
762
|
-
const head = segments[0];
|
|
763
|
-
if ((head === 'src' || head === 'app') && segments.length > 1)
|
|
764
|
-
return [segments, segments.slice(1)];
|
|
765
|
-
return [segments];
|
|
766
|
-
}
|
|
767
|
-
function aliasesCanMatchSameFile(left, right) {
|
|
768
|
-
const leftVariants = aliasGlobVariants(left);
|
|
769
|
-
const rightVariants = aliasGlobVariants(right);
|
|
770
|
-
for (const a of leftVariants) {
|
|
771
|
-
if (globsCanMatchSame(a, right, 0, 0, new Map()))
|
|
772
|
-
return true;
|
|
773
|
-
}
|
|
774
|
-
for (const b of rightVariants) {
|
|
775
|
-
if (globsCanMatchSame(left, b, 0, 0, new Map()))
|
|
776
|
-
return true;
|
|
777
|
-
}
|
|
778
|
-
return false;
|
|
779
|
-
}
|
|
780
|
-
function globsCanMatchSame(left, right, i, j, memo) {
|
|
781
|
-
const key = `${i}:${j}`;
|
|
782
|
-
const cached = memo.get(key);
|
|
783
|
-
if (cached !== undefined)
|
|
784
|
-
return cached;
|
|
785
|
-
let matched = false;
|
|
786
|
-
if (i === left.length && j === right.length)
|
|
787
|
-
matched = true;
|
|
788
|
-
else if (i < left.length && left[i] === '**') {
|
|
789
|
-
matched =
|
|
790
|
-
globsCanMatchSame(left, right, i + 1, j, memo) ||
|
|
791
|
-
(j < right.length && globsCanMatchSame(left, right, i, j + 1, memo));
|
|
792
|
-
}
|
|
793
|
-
else if (j < right.length && right[j] === '**') {
|
|
794
|
-
matched =
|
|
795
|
-
globsCanMatchSame(left, right, i, j + 1, memo) ||
|
|
796
|
-
(i < left.length && globsCanMatchSame(left, right, i + 1, j, memo));
|
|
797
|
-
}
|
|
798
|
-
else if (i < left.length && j < right.length) {
|
|
799
|
-
const a = left[i];
|
|
800
|
-
const b = right[j];
|
|
801
|
-
if (a === b || a === '*' || b === '*')
|
|
802
|
-
matched = globsCanMatchSame(left, right, i + 1, j + 1, memo);
|
|
803
|
-
}
|
|
804
|
-
memo.set(key, matched);
|
|
805
|
-
return matched;
|
|
806
|
-
}
|
|
807
|
-
function validateChildSliceAliases(candidate, issues) {
|
|
808
|
-
const rules = candidate.rules;
|
|
809
|
-
if (!Array.isArray(rules))
|
|
810
|
-
return;
|
|
811
|
-
rules.forEach((rule, index) => {
|
|
812
|
-
if (!isObject(rule))
|
|
813
|
-
return;
|
|
814
|
-
const child = rule.childSlices;
|
|
815
|
-
if (!isObject(child) || child.sliceAliases === undefined)
|
|
816
|
-
return;
|
|
817
|
-
const aliases = child.sliceAliases;
|
|
818
|
-
const path = `$.rules[${index}].childSlices.sliceAliases`;
|
|
819
|
-
if (!Array.isArray(aliases)) {
|
|
820
|
-
issues.push({ path, message: 'must be an array of { from, to } slice aliases' });
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
const shapes = universeShapes(rule.sliceFolders, rule.sliceIdentity);
|
|
824
|
-
const sliceEntries = [
|
|
825
|
-
...(Array.isArray(rule.sliceFolders) ? rule.sliceFolders : []),
|
|
826
|
-
...(Array.isArray(child.sliceFolders) ? child.sliceFolders : []),
|
|
827
|
-
].filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
828
|
-
const globs = [];
|
|
829
|
-
aliases.forEach((alias, aliasIndex) => {
|
|
830
|
-
const aliasPath = `${path}[${aliasIndex}]`;
|
|
831
|
-
if (!isObject(alias)) {
|
|
832
|
-
issues.push({ path: aliasPath, message: 'must be an object with from and to' });
|
|
309
|
+
const seen = new Set();
|
|
310
|
+
value.forEach((entry, index) => {
|
|
311
|
+
const itemPath = `${path}[${index}]`;
|
|
312
|
+
if (typeof entry !== 'string' || entry.length === 0) {
|
|
313
|
+
issues.push({ path: itemPath, message: 'must be a non-empty relative path string' });
|
|
833
314
|
return;
|
|
834
315
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
const from = alias.from;
|
|
841
|
-
const to = alias.to;
|
|
842
|
-
if (typeof from !== 'string' || from.trim().length === 0) {
|
|
843
|
-
issues.push({ path: `${aliasPath}.from`, message: 'must be a non-empty path glob' });
|
|
844
|
-
}
|
|
845
|
-
else {
|
|
846
|
-
const issue = aliasGlobIssue(from);
|
|
847
|
-
if (issue)
|
|
848
|
-
issues.push({ path: `${aliasPath}.from`, message: issue });
|
|
849
|
-
else {
|
|
850
|
-
const segments = aliasPathSegments(from);
|
|
851
|
-
if (sliceEntries.some((entry) => globOverlapsSliceEntry(segments, entry))) {
|
|
852
|
-
issues.push({ path: `${aliasPath}.from`, message: ALIAS_OVERLAP_MESSAGE });
|
|
853
|
-
}
|
|
854
|
-
for (const previous of globs) {
|
|
855
|
-
if (aliasesCanMatchSameFile(previous, segments)) {
|
|
856
|
-
issues.push({ path: aliasPath, message: ALIAS_SAME_FILE_MESSAGE });
|
|
857
|
-
break;
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
globs.push(segments);
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
if (typeof to !== 'string' || to.trim().length === 0) {
|
|
864
|
-
issues.push({ path: `${aliasPath}.to`, message: ALIAS_TARGET_MESSAGE });
|
|
865
|
-
}
|
|
866
|
-
else {
|
|
867
|
-
const issue = aliasTargetIssue(to, shapes);
|
|
868
|
-
if (issue)
|
|
869
|
-
issues.push({ path: `${aliasPath}.to`, message: issue });
|
|
870
|
-
}
|
|
316
|
+
if (seen.has(entry))
|
|
317
|
+
issues.push({ path: itemPath, message: 'duplicate path' });
|
|
318
|
+
seen.add(entry);
|
|
871
319
|
});
|
|
872
|
-
}
|
|
320
|
+
}
|
|
873
321
|
}
|
|
874
322
|
function validateLayerOwners(candidate, issues) {
|
|
875
323
|
const layers = candidate.layers;
|
|
@@ -997,13 +445,12 @@ export function migrateArkConfig(input, source = 'ark.config.json') {
|
|
|
997
445
|
export function loadArkConfigContract(input, source = 'ark.config.json') {
|
|
998
446
|
const { candidate, migratedFrom } = migrateArkConfig(input, source);
|
|
999
447
|
const issues = [];
|
|
1000
|
-
|
|
448
|
+
validateSchemaNode(candidate, ARK_CONFIG_SCHEMA, '$', ARK_CONFIG_SCHEMA, issues);
|
|
1001
449
|
validateArkRunExtra(candidate, issues);
|
|
1002
450
|
validateArkOrderExtra(candidate, issues);
|
|
1003
451
|
validateLayerOwners(candidate, issues);
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
validateChildSliceAliases(candidate, issues);
|
|
452
|
+
validateSliceContract(candidate, issues);
|
|
453
|
+
validateArkRulesRefs(candidate, issues);
|
|
1007
454
|
if (issues.length > 0)
|
|
1008
455
|
throw new ArkConfigValidationError(source, issues);
|
|
1009
456
|
return { config: candidate, migratedFrom };
|
|
@@ -1036,15 +483,3 @@ export function withArkConfigMetadata(config) {
|
|
|
1036
483
|
}
|
|
1037
484
|
return result;
|
|
1038
485
|
}
|
|
1039
|
-
/**
|
|
1040
|
-
* Trim trailing slashes without a regex.
|
|
1041
|
-
*
|
|
1042
|
-
* `/\/+$/` is a polynomial ReDoS on a value that comes from the repo's own
|
|
1043
|
-
* contract but is still library input. A scan is linear and says the same thing.
|
|
1044
|
-
*/
|
|
1045
|
-
function trimTrailingSlashes(value) {
|
|
1046
|
-
let end = value.length;
|
|
1047
|
-
while (end > 0 && value[end - 1] === '/')
|
|
1048
|
-
end -= 1;
|
|
1049
|
-
return value.slice(0, end);
|
|
1050
|
-
}
|