arkgate 4.8.24 → 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 +85 -0
- package/README.md +2 -2
- package/bin/ark-check-runtime.mjs +17 -14
- package/bin/ark-layer-match.mjs +115 -13
- package/bin/ark-mcp-runtime.mjs +36 -31
- package/bin/ark-shared.mjs +4 -23
- 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 +4 -4
- package/bin/lib/ambient-state.mjs +9 -30
- package/bin/lib/analysis-engine.mjs +14 -14
- package/bin/lib/architecture-scan.mjs +11 -9
- package/bin/lib/ark-order-doctor.mjs +3 -28
- package/bin/lib/ark-order-facts.mjs +6 -99
- package/bin/lib/ark-order-types.mjs +1 -2
- package/bin/lib/ark-run-doctor.mjs +3 -28
- package/bin/lib/ark-run-facts.mjs +30 -40
- package/bin/lib/ark-run-sensors.mjs +1 -1
- package/bin/lib/arkrule-file-hints.mjs +2 -47
- package/bin/lib/arkrules-contract.mjs +124 -133
- package/bin/lib/arkrules-sensors.mjs +7 -5
- package/bin/lib/check-args.mjs +63 -1
- package/bin/lib/ci-and-commands.mjs +8 -72
- package/bin/lib/ci-merge-boundary.mjs +1 -1
- package/bin/lib/class-source-scan.mjs +5 -5
- package/bin/lib/clone-detection.mjs +474 -0
- package/bin/lib/codex-home.mjs +1 -1
- package/bin/lib/config-contract-slices.mjs +44 -6
- package/bin/lib/config-contract.mjs +40 -117
- package/bin/lib/config-extras.mjs +16 -22
- package/bin/lib/contract-smells.mjs +33 -15
- package/bin/lib/core-ratchet.mjs +0 -2
- package/bin/lib/dashboard-cli.mjs +2 -2
- package/bin/lib/deep-module-coach.mjs +2 -4
- package/bin/lib/deploy-path.mjs +13 -38
- package/bin/lib/design-smells.mjs +2 -2
- package/bin/lib/diagnostic-catalog.mjs +8 -1
- package/bin/lib/doctor-advisories.mjs +55 -9
- package/bin/lib/doctor-green-cite.mjs +1 -1
- package/bin/lib/doctor-human.mjs +1 -1
- package/bin/lib/doctor-next-actions.mjs +2 -2
- package/bin/lib/doctor-plan.mjs +4 -6
- package/bin/lib/duplication-io.mjs +572 -0
- package/bin/lib/effective-contract-load.mjs +181 -44
- 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 +7 -3
- package/bin/lib/flat-parent-importers.mjs +5 -3
- package/bin/lib/gate-files.mjs +30 -89
- package/bin/lib/github-enforcement.mjs +165 -28
- package/bin/lib/golden-pattern.mjs +1 -1
- package/bin/lib/hook-templates.mjs +16 -16
- 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/improvement-compass-doctor.mjs +1 -24
- package/bin/lib/invariant-coverage-io.mjs +4 -3
- package/bin/lib/invariant-coverage.mjs +26 -0
- 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 +4 -5
- package/bin/lib/mcp-hook-payload.mjs +4 -4
- 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 +3 -3
- package/bin/lib/parse-health.mjs +1 -1
- package/bin/lib/persistence-write-hint.mjs +33 -8
- package/bin/lib/pin-dependent-gates.mjs +2 -2
- package/bin/lib/policy-delta-io.mjs +55 -8
- package/bin/lib/post-green-path.mjs +1 -1
- package/bin/lib/prepare-change.mjs +3 -9
- package/bin/lib/prepare-write.mjs +1 -2
- package/bin/lib/presets.mjs +56 -68
- package/bin/lib/product-copy.mjs +1 -6
- package/bin/lib/project-root.mjs +31 -62
- package/bin/lib/resolved-candidate-facts.mjs +90 -62
- package/bin/lib/rules-under-contract.mjs +72 -8
- package/bin/lib/scan-files.mjs +4 -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 +1 -1
- package/bin/lib/start-install-recovery.mjs +1 -1
- package/bin/lib/start-preview.mjs +1 -1
- package/bin/lib/states-transitions-presence.mjs +3 -3
- package/bin/lib/status-command.mjs +2 -18
- package/bin/lib/status-manifest.mjs +18 -26
- package/bin/lib/status-transition-catalog.mjs +0 -6
- package/bin/lib/suggestions.mjs +3 -3
- package/bin/lib/team-parliament-io.mjs +3 -17
- package/bin/lib/team-parliament.mjs +15 -9
- 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 +2 -2
- package/dist/chunk-AHACEKSV.js +16 -0
- package/dist/chunk-UN5EH4QV.cjs +16 -0
- package/dist/{configTypes-BJbaeoj4.d.cts → configTypes-GPvvdleM.d.cts} +12 -1
- package/dist/{configTypes-BJbaeoj4.d.ts → configTypes-GPvvdleM.d.ts} +12 -1
- package/dist/eslint/index.cjs +11 -11
- package/dist/eslint/index.d.cts +10 -1
- package/dist/eslint/index.d.ts +10 -1
- package/dist/eslint/index.js +11 -11
- package/dist/index.cjs +34 -34
- package/dist/index.d.cts +587 -515
- package/dist/index.d.ts +587 -515
- package/dist/index.js +34 -34
- package/dist/nestjs/index.cjs +1 -1
- package/dist/nestjs/index.d.cts +1 -1
- package/dist/nestjs/index.d.ts +1 -1
- package/dist/nestjs/index.js +1 -1
- package/dist/order/index.cjs +1 -1
- package/dist/order/index.js +1 -1
- package/dist/runtime/index.cjs +12 -12
- package/dist/runtime/index.d.cts +174 -136
- package/dist/runtime/index.d.ts +174 -136
- package/dist/runtime/index.js +12 -12
- package/dist/{types-DRinG-vT.d.cts → types-CnauMQQL.d.cts} +33 -25
- package/dist/{types-DRinG-vT.d.ts → types-CnauMQQL.d.ts} +33 -25
- package/docs/README.md +2 -1
- package/docs/agent-guide.md +134 -2
- package/docs/ai-gates.md +15 -1
- package/docs/configuration.md +40 -11
- package/docs/diagnostics.md +118 -2
- package/docs/package-surface.md +4 -1
- package/docs/product-voice.md +7 -1
- package/package.json +11 -2
- package/schemas/ark.config.schema.json +31 -3
- package/schemas/ark.invariant-probe.schema.json +368 -0
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +1 -0
- 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-place/SKILL.md +8 -0
- package/templates/skills/ark-adopt.md +1 -0
- 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-place.md +8 -0
- package/bin/lib/ark-order-error.mjs +0 -18
- package/bin/lib/ark-order-invariants.mjs +0 -388
- package/bin/lib/stable-hash.mjs +0 -90
- package/dist/chunk-MBXCFLO4.js +0 -16
- package/dist/chunk-RNQHRDCQ.cjs +0 -16
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
directoryArkRulesReader,
|
|
8
8
|
loadEffectiveArkRules,
|
|
9
9
|
loadEffectiveArkRulesFromDisk,
|
|
10
|
+
normalizeProjectRelativePath,
|
|
10
11
|
objectArkRulesReader,
|
|
11
12
|
} from './effective-contract-load.mjs';
|
|
12
13
|
import {
|
|
@@ -15,7 +16,9 @@ import {
|
|
|
15
16
|
loadInvariantCoverageInputs,
|
|
16
17
|
} from './invariant-coverage-io.mjs';
|
|
17
18
|
import { evaluateInvariantCoverage } from './invariant-coverage.mjs';
|
|
19
|
+
import { attachProbeEvidence } from './invariant-probe-io.mjs';
|
|
18
20
|
import { attachPolicyAdrNote } from './adr-path.mjs';
|
|
21
|
+
import { collectGovernedFiles } from './scan-files.mjs';
|
|
19
22
|
|
|
20
23
|
function readJsonFile(filePath, label) {
|
|
21
24
|
if (!fs.existsSync(filePath)) throw new Error(`${label} not found: ${filePath}`);
|
|
@@ -95,6 +98,20 @@ function configPathInRepository(root, configPath, top) {
|
|
|
95
98
|
* ArkRules files at the base ref, relative to the project root inside the repository.
|
|
96
99
|
* A path absent at the ref (added by the candidate) reads as `missing`.
|
|
97
100
|
*/
|
|
101
|
+
function baseRefSourceFiles(top, ref, prefix) {
|
|
102
|
+
const shown = runGit(top, ['ls-tree', '-r', '--name-only', ref]);
|
|
103
|
+
if (shown.status !== 0) return [];
|
|
104
|
+
const rootPrefix = prefix ? `${prefix}/` : '';
|
|
105
|
+
const files = [];
|
|
106
|
+
for (const line of shown.stdout.split('\n')) {
|
|
107
|
+
if (!line) continue;
|
|
108
|
+
if (rootPrefix && !line.startsWith(rootPrefix)) continue;
|
|
109
|
+
const rel = rootPrefix ? line.slice(rootPrefix.length) : line;
|
|
110
|
+
if (/\.[cm]?[tj]sx?$/.test(rel) && !rel.endsWith('.d.ts')) files.push(rel);
|
|
111
|
+
}
|
|
112
|
+
return files;
|
|
113
|
+
}
|
|
114
|
+
|
|
98
115
|
function baseRefArkRulesReader(root, top, ref) {
|
|
99
116
|
const prefix = path
|
|
100
117
|
.relative(fs.realpathSync(top), fs.realpathSync(root))
|
|
@@ -102,6 +119,7 @@ function baseRefArkRulesReader(root, top, ref) {
|
|
|
102
119
|
.join('/');
|
|
103
120
|
const spec = (rel) => `${ref}:${prefix ? `${prefix}/` : ''}${rel}`;
|
|
104
121
|
return {
|
|
122
|
+
files: baseRefSourceFiles(top, ref, prefix),
|
|
105
123
|
readArkRulesFile: (rel) => {
|
|
106
124
|
const shown = runGit(top, ['show', spec(rel)]);
|
|
107
125
|
if (shown.status === 0) return { ok: true, content: shown.stdout };
|
|
@@ -131,11 +149,16 @@ export function resolvePolicyBaseConfig({
|
|
|
131
149
|
// A base config FILE carries its ArkRules next to it (same relative paths as the
|
|
132
150
|
// project), never from the candidate working tree: that would compare the
|
|
133
151
|
// candidate catalog with itself and hide every demotion/deletion.
|
|
152
|
+
const baseRoot = fs.realpathSync(path.dirname(absolute));
|
|
153
|
+
const include = Array.isArray(config.include) && config.include.length > 0 ? config.include : ['src'];
|
|
134
154
|
return {
|
|
135
155
|
config,
|
|
136
156
|
source: absolute,
|
|
137
157
|
ref: null,
|
|
138
|
-
|
|
158
|
+
files: collectGovernedFiles(baseRoot, { ...config, include })
|
|
159
|
+
.map((file) => path.relative(baseRoot, file).split(path.sep).join('/'))
|
|
160
|
+
.filter((rel) => rel && !rel.startsWith('..')),
|
|
161
|
+
readArkRulesFile: directoryArkRulesReader(baseRoot),
|
|
139
162
|
};
|
|
140
163
|
}
|
|
141
164
|
|
|
@@ -198,9 +221,10 @@ export function readPolicyAcknowledgement(root, acknowledgementPath) {
|
|
|
198
221
|
* failure is fail-closed: silently dropping the base catalog would make every
|
|
199
222
|
* ArkRule demotion or deletion classify as neutral.
|
|
200
223
|
*/
|
|
201
|
-
|
|
224
|
+
function loadBaseArkRules(base) {
|
|
202
225
|
const loaded = loadEffectiveArkRules(base.config, base.readArkRulesFile, {
|
|
203
226
|
missingAsEmpty: base.ref != null,
|
|
227
|
+
...(Array.isArray(base.files) ? { files: base.files } : {}),
|
|
204
228
|
});
|
|
205
229
|
if (loaded.errors.length > 0) {
|
|
206
230
|
const message = loaded.errors.map((issue) => `- ${issue.path}: ${issue.message}`).join('\n');
|
|
@@ -220,6 +244,17 @@ function hasArkRulesMap(config) {
|
|
|
220
244
|
return Boolean(refs && typeof refs === 'object' && Object.keys(refs).length > 0);
|
|
221
245
|
}
|
|
222
246
|
|
|
247
|
+
/** Paths in a supplied catalog. A slice rule file is enough for the child wall to name it. */
|
|
248
|
+
function catalogPaths(files) {
|
|
249
|
+
if (!files || typeof files !== 'object') return [];
|
|
250
|
+
const out = [];
|
|
251
|
+
for (const key of Object.keys(files)) {
|
|
252
|
+
const rel = normalizeProjectRelativePath(String(key));
|
|
253
|
+
if (rel) out.push(rel);
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
223
258
|
function loadedOrThrow(loaded, label) {
|
|
224
259
|
if (loaded.errors.length === 0) return loaded.arkRules;
|
|
225
260
|
const message = loaded.errors.map((issue) => `- ${issue.path}: ${issue.message}`).join('\n');
|
|
@@ -275,7 +310,9 @@ export function resolvePolicyDeltaArkRules({
|
|
|
275
310
|
);
|
|
276
311
|
}
|
|
277
312
|
baseArkRules = loadedOrThrow(
|
|
278
|
-
loadEffectiveArkRules(baseConfig, objectArkRulesReader(baseArkRuleFiles)
|
|
313
|
+
loadEffectiveArkRules(baseConfig, objectArkRulesReader(baseArkRuleFiles), {
|
|
314
|
+
files: catalogPaths(baseArkRuleFiles),
|
|
315
|
+
}),
|
|
279
316
|
'Base'
|
|
280
317
|
);
|
|
281
318
|
} else {
|
|
@@ -285,7 +322,9 @@ export function resolvePolicyDeltaArkRules({
|
|
|
285
322
|
let candidateArkRules;
|
|
286
323
|
if (candidateArkRuleFiles && typeof candidateArkRuleFiles === 'object') {
|
|
287
324
|
candidateArkRules = loadedOrThrow(
|
|
288
|
-
loadEffectiveArkRules(candidateConfig, objectArkRulesReader(candidateArkRuleFiles)
|
|
325
|
+
loadEffectiveArkRules(candidateConfig, objectArkRulesReader(candidateArkRuleFiles), {
|
|
326
|
+
files: catalogPaths(candidateArkRuleFiles),
|
|
327
|
+
}),
|
|
289
328
|
'Candidate'
|
|
290
329
|
);
|
|
291
330
|
} else if (candidateIsProjectConfig) {
|
|
@@ -347,12 +386,20 @@ export function analyzePolicyTransition({
|
|
|
347
386
|
fileContents: coverageInputs.fileContents,
|
|
348
387
|
testFiles: coverageInputs.testFiles,
|
|
349
388
|
testGlobsMissing: coverageInputs.testGlobsMissing,
|
|
350
|
-
// No coverageStats / coverageRoots: this caller reads coverage ROWS and
|
|
351
|
-
// drops the violations, and both only shape violation messages. Passing
|
|
352
|
-
// them would look like wiring while changing nothing observable here.
|
|
353
389
|
coverageBudgetExhausted: coverageInputs.coverageBudgetExhausted === true,
|
|
390
|
+
...(coverageInputs.stats ? { coverageStats: coverageInputs.stats } : {}),
|
|
391
|
+
// The declared roots set `coverageRootsDeclared` / `outsideDeclaredRoots`
|
|
392
|
+
// on each row, and canPromoteInvariant refuses without them. Same inputs
|
|
393
|
+
// as `--promote` (sensor-promote-io), so both surfaces judge alike.
|
|
394
|
+
...(coverageInputs.coverageRoots ? { coverageRoots: coverageInputs.coverageRoots } : {}),
|
|
354
395
|
});
|
|
355
|
-
|
|
396
|
+
// ADR 0039: the same committed probe evidence --promote reads, so both
|
|
397
|
+
// surfaces refuse alike. Read-only; nothing here runs a test.
|
|
398
|
+
candidateInvariantCoverage = attachProbeEvidence(
|
|
399
|
+
root,
|
|
400
|
+
evaluated.coverage,
|
|
401
|
+
candidateArkRules.invariants
|
|
402
|
+
).rows;
|
|
356
403
|
}
|
|
357
404
|
|
|
358
405
|
const acknowledgement = readPolicyAcknowledgement(root, acknowledgementPath);
|
|
@@ -21,7 +21,7 @@ export const POST_GREEN_PRIMARY_SKILL = '/ark-explore';
|
|
|
21
21
|
export const POST_GREEN_PRIMARY_ACTION = POST_GREEN_HUMAN;
|
|
22
22
|
|
|
23
23
|
/** Short label for tables / metrics. */
|
|
24
|
-
|
|
24
|
+
const POST_GREEN_PRIMARY_SHORT =
|
|
25
25
|
'/ark-explore shape-focus → /ark-autopilot (apply one B pilot) # Shape residual';
|
|
26
26
|
|
|
27
27
|
/** Placement coaching while design residual remains (new code only). */
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
import { effectiveAnalysisConfig } from './analysis-policy.mjs';
|
|
16
16
|
import { isScanExcludedRelative } from '../ark-shared.mjs';
|
|
17
17
|
import { classifyChangeSet, evaluateTeamGate } from './team-parliament.mjs';
|
|
18
|
-
import { loadEffectiveArkRulesFromDisk } from './effective-contract-load.mjs';
|
|
18
|
+
import { arkRulesLoadFailed, loadEffectiveArkRulesFromDisk } from './effective-contract-load.mjs';
|
|
19
19
|
import {
|
|
20
20
|
coverageOptionsFromConfig,
|
|
21
21
|
invariantIdsFromCatalog,
|
|
@@ -83,7 +83,7 @@ function assertInsideProject(root, relativePath) {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
function normalizeChangeSet(input) {
|
|
87
87
|
if (!Array.isArray(input)) throw new Error('changes must be an array.');
|
|
88
88
|
return input.map((change, index) => {
|
|
89
89
|
if (!change || typeof change !== 'object' || Array.isArray(change)) {
|
|
@@ -201,13 +201,7 @@ export function prepareChangeFromRoot({
|
|
|
201
201
|
// and a broken reference fails closed instead of silently dropping the catalog.
|
|
202
202
|
const arkRulesLoad = loadEffectiveArkRulesFromDisk(root, effectiveConfig);
|
|
203
203
|
if (arkRulesLoad.errors.length > 0) {
|
|
204
|
-
|
|
205
|
-
.map((issue) => (typeof issue === 'string' ? issue : `- ${issue.path}: ${issue.message}`))
|
|
206
|
-
.join('\n');
|
|
207
|
-
const error = new Error(`Invalid Effective Contract (${contractSource}):\n${message}`);
|
|
208
|
-
error.code = 'ARKRULES_LOAD_FAILED';
|
|
209
|
-
error.issues = arkRulesLoad.errors;
|
|
210
|
-
throw error;
|
|
204
|
+
throw arkRulesLoadFailed(contractSource, arkRulesLoad.errors);
|
|
211
205
|
}
|
|
212
206
|
const contract = loadContract(effectiveConfig, contractSource, {
|
|
213
207
|
arkRules: arkRulesLoad.arkRules,
|
|
@@ -8,11 +8,10 @@ import crypto from 'node:crypto';
|
|
|
8
8
|
import { validateWithAutoPatch } from './auto-patch.mjs';
|
|
9
9
|
import { classifyRemediation, enrichViolationWithFixClass } from './remediation.mjs';
|
|
10
10
|
import {
|
|
11
|
-
layerDescriptionCaption,
|
|
12
11
|
placementDescriptionFields,
|
|
13
12
|
} from './layer-description.mjs';
|
|
14
13
|
|
|
15
|
-
export {
|
|
14
|
+
export { placementDescriptionFields };
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
17
|
* Stable content identity for host commit / cache keys.
|
package/bin/lib/presets.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import { withArkConfigMetadata } from './config-contract.mjs';
|
|
|
19
19
|
const PRESETS_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
20
20
|
const ARKRULES_TEMPLATES_DIR = path.join(PRESETS_DIR, '../../templates/arkrules');
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
function denyUpward(names) {
|
|
23
23
|
const rules = [];
|
|
24
24
|
for (let i = 0; i < names.length; i += 1) {
|
|
25
25
|
for (let j = i + 1; j < names.length; j += 1) {
|
|
@@ -33,7 +33,7 @@ export function denyUpward(names) {
|
|
|
33
33
|
* peerIsolation matrix: deny only when importer/importee sit under different slices.
|
|
34
34
|
* Covers same-layer and cross-layer pairs (honest DDD / vertical-slice isolation).
|
|
35
35
|
*/
|
|
36
|
-
|
|
36
|
+
function peerIsolationEdges(layerNames, sliceFolders, message) {
|
|
37
37
|
const rules = [];
|
|
38
38
|
for (const from of layerNames) {
|
|
39
39
|
for (const to of layerNames) {
|
|
@@ -57,7 +57,7 @@ export function peerIsolationEdges(layerNames, sliceFolders, message) {
|
|
|
57
57
|
// Framework internals under `src/kernel/**` are NOT application architecture — a broad
|
|
58
58
|
// `src/**/domain/**` would otherwise swallow `src/kernel/domain`. Do NOT use `**/kernel/**`
|
|
59
59
|
// (that carves out legitimate `src/shared/kernel/**` SharedKernel paths).
|
|
60
|
-
|
|
60
|
+
const FRAMEWORK_INTERNAL_EXCLUDE = ['src/kernel/**', '**/src/kernel/**'];
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* Shared high-spec domain globs. Prefer these over Application/Presentation scatter
|
|
@@ -217,18 +217,6 @@ export const APPLICATION_LIB_ORCHESTRATION_PATTERNS = Object.freeze([
|
|
|
217
217
|
'**/lib/api-handlers/**',
|
|
218
218
|
]);
|
|
219
219
|
|
|
220
|
-
/**
|
|
221
|
-
* AR08 — attach lean arkRules map. Keys are always exact project layer names.
|
|
222
|
-
* Sensor roles (domain-structure / orchestration / adapter-thin / generic) are
|
|
223
|
-
* independent of display names so renamed layers still get the right starter.
|
|
224
|
-
*/
|
|
225
|
-
export const DEFAULT_ARKRULES_REFS = {
|
|
226
|
-
DomainModel: 'arkrules/DomainModel.json',
|
|
227
|
-
ApplicationOrchestration: 'arkrules/ApplicationOrchestration.json',
|
|
228
|
-
PresentationAdapters: 'arkrules/PresentationAdapters.json',
|
|
229
|
-
PersistenceAdapters: 'arkrules/PersistenceAdapters.json',
|
|
230
|
-
};
|
|
231
|
-
|
|
232
220
|
/** Sensor roles used when selecting or synthesizing per-layer templates. */
|
|
233
221
|
export const ARKRULES_SENSOR_ROLES = Object.freeze({
|
|
234
222
|
DOMAIN_STRUCTURE: 'domain-structure',
|
|
@@ -241,7 +229,7 @@ export const ARKRULES_SENSOR_ROLES = Object.freeze({
|
|
|
241
229
|
* Exact-name aliases → sensor role. Prefer this table over heuristics when the
|
|
242
230
|
* project uses a known vocabulary (hexagonal, monorepo field renames, etc.).
|
|
243
231
|
*/
|
|
244
|
-
|
|
232
|
+
const LAYER_SENSOR_ROLE_ALIASES = Object.freeze({
|
|
245
233
|
// Domain / pure model
|
|
246
234
|
DomainModel: ARKRULES_SENSOR_ROLES.DOMAIN_STRUCTURE,
|
|
247
235
|
Domain: ARKRULES_SENSOR_ROLES.DOMAIN_STRUCTURE,
|
|
@@ -448,7 +436,46 @@ export function writeArkRulesTemplates(root, config, { force = false } = {}) {
|
|
|
448
436
|
return written;
|
|
449
437
|
}
|
|
450
438
|
|
|
451
|
-
|
|
439
|
+
/**
|
|
440
|
+
* Deny edges shared by the ports-and-adapters presets after their DomainModel
|
|
441
|
+
* rules: orchestration never reaches adapters, adapters never reach each other,
|
|
442
|
+
* and presentation never reaches the domain directly. Fresh objects per call.
|
|
443
|
+
*/
|
|
444
|
+
function portsAndAdaptersRules() {
|
|
445
|
+
return [
|
|
446
|
+
{ from: 'ApplicationOrchestration', to: 'PersistenceAdapters', allowed: false },
|
|
447
|
+
{ from: 'ApplicationOrchestration', to: 'PresentationAdapters', allowed: false },
|
|
448
|
+
{ from: 'PresentationAdapters', to: 'PersistenceAdapters', allowed: false },
|
|
449
|
+
{ from: 'PresentationAdapters', to: 'DomainModel', allowed: false },
|
|
450
|
+
{ from: 'PersistenceAdapters', to: 'ApplicationOrchestration', allowed: false },
|
|
451
|
+
{ from: 'PersistenceAdapters', to: 'PresentationAdapters', allowed: false },
|
|
452
|
+
];
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* UI-surface preset rules: the domain and orchestration stay out of the UI, while
|
|
457
|
+
* presentation may reach data clients on day one (soft, `message` explains why).
|
|
458
|
+
* Persistence → Presentation stays denied. Fresh objects per call.
|
|
459
|
+
* @param {string} presentationToPersistenceMessage
|
|
460
|
+
*/
|
|
461
|
+
function uiSurfaceRules(presentationToPersistenceMessage) {
|
|
462
|
+
return [
|
|
463
|
+
{ from: 'DomainModel', to: 'PresentationAdapters', allowed: false },
|
|
464
|
+
{ from: 'DomainModel', to: 'PersistenceAdapters', allowed: false },
|
|
465
|
+
{ from: 'DomainModel', to: 'ApplicationOrchestration', allowed: false },
|
|
466
|
+
{ from: 'ApplicationOrchestration', to: 'PresentationAdapters', allowed: false },
|
|
467
|
+
{
|
|
468
|
+
from: 'PresentationAdapters',
|
|
469
|
+
to: 'PersistenceAdapters',
|
|
470
|
+
allowed: true,
|
|
471
|
+
message: presentationToPersistenceMessage,
|
|
472
|
+
},
|
|
473
|
+
{ from: 'PersistenceAdapters', to: 'PresentationAdapters', allowed: false },
|
|
474
|
+
{ from: 'PersistenceAdapters', to: 'ApplicationOrchestration', allowed: false },
|
|
475
|
+
];
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function presetWithOverlays(baseConfig, root) {
|
|
452
479
|
const config = root ? applyFrameworkLayoutOverlays(baseConfig, root) : baseConfig;
|
|
453
480
|
return withArkConfigMetadata(withDefaultArkRules(config));
|
|
454
481
|
}
|
|
@@ -511,12 +538,7 @@ export const ARCHITECTURE_PRESETS = {
|
|
|
511
538
|
{ from: 'DomainModel', to: 'ApplicationOrchestration', allowed: false },
|
|
512
539
|
{ from: 'DomainModel', to: 'PersistenceAdapters', allowed: false },
|
|
513
540
|
{ from: 'DomainModel', to: 'PresentationAdapters', allowed: false },
|
|
514
|
-
|
|
515
|
-
{ from: 'ApplicationOrchestration', to: 'PresentationAdapters', allowed: false },
|
|
516
|
-
{ from: 'PresentationAdapters', to: 'PersistenceAdapters', allowed: false },
|
|
517
|
-
{ from: 'PresentationAdapters', to: 'DomainModel', allowed: false },
|
|
518
|
-
{ from: 'PersistenceAdapters', to: 'ApplicationOrchestration', allowed: false },
|
|
519
|
-
{ from: 'PersistenceAdapters', to: 'PresentationAdapters', allowed: false },
|
|
541
|
+
...portsAndAdaptersRules(),
|
|
520
542
|
],
|
|
521
543
|
},
|
|
522
544
|
root
|
|
@@ -705,12 +727,7 @@ export const ARCHITECTURE_PRESETS = {
|
|
|
705
727
|
{ from: 'DomainModel', to: 'ApplicationOrchestration', allowed: false },
|
|
706
728
|
{ from: 'DomainModel', to: 'PresentationAdapters', allowed: false },
|
|
707
729
|
{ from: 'DomainModel', to: 'PersistenceAdapters', allowed: false },
|
|
708
|
-
|
|
709
|
-
{ from: 'ApplicationOrchestration', to: 'PresentationAdapters', allowed: false },
|
|
710
|
-
{ from: 'PresentationAdapters', to: 'PersistenceAdapters', allowed: false },
|
|
711
|
-
{ from: 'PresentationAdapters', to: 'DomainModel', allowed: false },
|
|
712
|
-
{ from: 'PersistenceAdapters', to: 'ApplicationOrchestration', allowed: false },
|
|
713
|
-
{ from: 'PersistenceAdapters', to: 'PresentationAdapters', allowed: false },
|
|
730
|
+
...portsAndAdaptersRules(),
|
|
714
731
|
],
|
|
715
732
|
},
|
|
716
733
|
root
|
|
@@ -810,24 +827,12 @@ export const ARCHITECTURE_PRESETS = {
|
|
|
810
827
|
optional: true,
|
|
811
828
|
},
|
|
812
829
|
],
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
// ui-surface keeps this as a soft guidance edge (allowed) until ports exist —
|
|
820
|
-
// Persistence → Presentation stays denied when that edge appears.
|
|
821
|
-
{
|
|
822
|
-
from: 'PresentationAdapters',
|
|
823
|
-
to: 'PersistenceAdapters',
|
|
824
|
-
allowed: true,
|
|
825
|
-
message:
|
|
826
|
-
'UI/routes may reach data clients on day one (RSC); prefer application ports as the product grows.',
|
|
827
|
-
},
|
|
828
|
-
{ from: 'PersistenceAdapters', to: 'PresentationAdapters', allowed: false },
|
|
829
|
-
{ from: 'PersistenceAdapters', to: 'ApplicationOrchestration', allowed: false },
|
|
830
|
-
],
|
|
830
|
+
// Next RSC often imports data clients from routes; deny is ideal but day-one
|
|
831
|
+
// ui-surface keeps this as a soft guidance edge (allowed) until ports exist —
|
|
832
|
+
// Persistence → Presentation stays denied when that edge appears.
|
|
833
|
+
rules: uiSurfaceRules(
|
|
834
|
+
'UI/routes may reach data clients on day one (RSC); prefer application ports as the product grows.'
|
|
835
|
+
),
|
|
831
836
|
},
|
|
832
837
|
root
|
|
833
838
|
),
|
|
@@ -978,12 +983,7 @@ export const ARCHITECTURE_PRESETS = {
|
|
|
978
983
|
{ from: 'DomainModel', to: 'ApplicationOrchestration', allowed: false },
|
|
979
984
|
{ from: 'DomainModel', to: 'PersistenceAdapters', allowed: false },
|
|
980
985
|
{ from: 'DomainModel', to: 'PresentationAdapters', allowed: false },
|
|
981
|
-
|
|
982
|
-
{ from: 'ApplicationOrchestration', to: 'PresentationAdapters', allowed: false },
|
|
983
|
-
{ from: 'PresentationAdapters', to: 'PersistenceAdapters', allowed: false },
|
|
984
|
-
{ from: 'PresentationAdapters', to: 'DomainModel', allowed: false },
|
|
985
|
-
{ from: 'PersistenceAdapters', to: 'ApplicationOrchestration', allowed: false },
|
|
986
|
-
{ from: 'PersistenceAdapters', to: 'PresentationAdapters', allowed: false },
|
|
986
|
+
...portsAndAdaptersRules(),
|
|
987
987
|
{ from: 'SharedKernel', to: 'DomainModel', allowed: false },
|
|
988
988
|
{ from: 'SharedKernel', to: 'ApplicationOrchestration', allowed: false },
|
|
989
989
|
{ from: 'SharedKernel', to: 'PresentationAdapters', allowed: false },
|
|
@@ -1078,21 +1078,9 @@ export const ARCHITECTURE_PRESETS = {
|
|
|
1078
1078
|
optional: true,
|
|
1079
1079
|
},
|
|
1080
1080
|
],
|
|
1081
|
-
rules:
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
{ from: 'DomainModel', to: 'ApplicationOrchestration', allowed: false },
|
|
1085
|
-
{ from: 'ApplicationOrchestration', to: 'PresentationAdapters', allowed: false },
|
|
1086
|
-
{
|
|
1087
|
-
from: 'PresentationAdapters',
|
|
1088
|
-
to: 'PersistenceAdapters',
|
|
1089
|
-
allowed: true,
|
|
1090
|
-
message:
|
|
1091
|
-
'SPA day-one UI may reach data clients; prefer ports as the product grows.',
|
|
1092
|
-
},
|
|
1093
|
-
{ from: 'PersistenceAdapters', to: 'PresentationAdapters', allowed: false },
|
|
1094
|
-
{ from: 'PersistenceAdapters', to: 'ApplicationOrchestration', allowed: false },
|
|
1095
|
-
],
|
|
1081
|
+
rules: uiSurfaceRules(
|
|
1082
|
+
'SPA day-one UI may reach data clients; prefer ports as the product grows.'
|
|
1083
|
+
),
|
|
1096
1084
|
},
|
|
1097
1085
|
root
|
|
1098
1086
|
),
|
package/bin/lib/product-copy.mjs
CHANGED
|
@@ -25,7 +25,7 @@ export const ARKRULES_FIRST_CONTACT_NEXT =
|
|
|
25
25
|
export const ARKRULES_EMPTY_CATALOG_NEXT =
|
|
26
26
|
'Next: add 1–2 short phrases to invariants[] in the Domain rules file, then ark-check --doctor.';
|
|
27
27
|
|
|
28
|
-
/** Status-light leftover-design qualifier (was “design-weak”). */
|
|
28
|
+
/** Status-light leftover-design qualifier (was “design-weak”); also the HTML/doctor badge text. */
|
|
29
29
|
export const LEFTOVER_DESIGN_LABEL = 'leftover design work';
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -41,14 +41,9 @@ export function operatingModeTitle(mode, leftoverDesign, stewardsUnset) {
|
|
|
41
41
|
return light;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/** Short HTML/doctor badge text. */
|
|
45
|
-
export const LEFTOVER_DESIGN_BADGE = LEFTOVER_DESIGN_LABEL;
|
|
46
|
-
|
|
47
44
|
export const POST_GREEN_HUMAN =
|
|
48
45
|
'Imports check out, but the design is still messy. Next: /ark-explore, then one small refactor with /ark-autopilot and your OK.';
|
|
49
46
|
|
|
50
47
|
export const POST_GREEN_LEDE =
|
|
51
48
|
'Import rules are clean, but leftover design work remains. That does not fail the check — it only means “done” is still wrong until you tidy shape.';
|
|
52
49
|
|
|
53
|
-
export const HARD_WRITE_HUMAN = 'pre-write block';
|
|
54
|
-
export const ADVISORY_WRITE_HUMAN = 'warning only (not blocked)';
|
package/bin/lib/project-root.mjs
CHANGED
|
@@ -16,14 +16,14 @@ import fs from 'node:fs';
|
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
|
|
18
18
|
/** Safety cap so a pathological tree cannot walk forever. */
|
|
19
|
-
|
|
19
|
+
const MAX_CONFIG_WALK_DEPTH = 32;
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* True when dir looks like a package-manager workspaces / monorepo root.
|
|
23
23
|
* Used as an upper bound for walk-up (config at this root is accepted; parents are not).
|
|
24
24
|
* @param {string} dir
|
|
25
25
|
*/
|
|
26
|
-
|
|
26
|
+
function isWorkspacesPackageRoot(dir) {
|
|
27
27
|
const pkgPath = path.join(dir, 'package.json');
|
|
28
28
|
try {
|
|
29
29
|
if (!fs.statSync(pkgPath, { throwIfNoEntry: false })?.isFile()) {
|
|
@@ -55,7 +55,7 @@ export function isWorkspacesPackageRoot(dir) {
|
|
|
55
55
|
* True when dir is a git worktree root (.git file or directory).
|
|
56
56
|
* @param {string} dir
|
|
57
57
|
*/
|
|
58
|
-
|
|
58
|
+
function isGitRoot(dir) {
|
|
59
59
|
try {
|
|
60
60
|
const git = path.join(dir, '.git');
|
|
61
61
|
const st = fs.statSync(git, { throwIfNoEntry: false });
|
|
@@ -97,7 +97,7 @@ function isFile(absPath) {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/** True when --config is a path (nested or absolute), not a basename to walk. */
|
|
100
|
-
|
|
100
|
+
function configNameIsPath(configName) {
|
|
101
101
|
return (
|
|
102
102
|
typeof configName === 'string' &&
|
|
103
103
|
(path.isAbsolute(configName) || /[\\/]/.test(configName))
|
|
@@ -118,7 +118,7 @@ function isInsideRoot(root, target) {
|
|
|
118
118
|
* @param {string} configName
|
|
119
119
|
* @returns {string | null} absolute config file path
|
|
120
120
|
*/
|
|
121
|
-
|
|
121
|
+
function resolveConfigPathCandidate(startDir, configName) {
|
|
122
122
|
if (typeof configName !== 'string' || configName.trim() === '') return null;
|
|
123
123
|
const start = path.resolve(startDir || process.cwd());
|
|
124
124
|
if (path.isAbsolute(configName)) return isFile(configName) ? path.resolve(configName) : null;
|
|
@@ -243,76 +243,45 @@ export function resolveEffectiveProjectRoot(startRoot, opts = {}) {
|
|
|
243
243
|
// Writes adopt walked config root only with explicit opt-in.
|
|
244
244
|
const adoptWalkedRoot = !writeMode || followConfigRoot;
|
|
245
245
|
|
|
246
|
+
/** Config found by walk-up (or at start); root follows it unless a plain write. */
|
|
247
|
+
const walkedResult = (found) => ({
|
|
248
|
+
root: adoptWalkedRoot ? found.root : start,
|
|
249
|
+
writeRoot: start,
|
|
250
|
+
config: configName,
|
|
251
|
+
configPath: found.configPath,
|
|
252
|
+
configRoot: found.root,
|
|
253
|
+
walkedUp: found.walkedUp,
|
|
254
|
+
configFound: true,
|
|
255
|
+
writeRootFollowedConfig: adoptWalkedRoot && found.walkedUp,
|
|
256
|
+
});
|
|
257
|
+
/** Everything stays at the start directory. */
|
|
258
|
+
const startResult = (configPath, configFound) => ({
|
|
259
|
+
root: start,
|
|
260
|
+
writeRoot: start,
|
|
261
|
+
config: configName,
|
|
262
|
+
configPath,
|
|
263
|
+
configRoot: start,
|
|
264
|
+
walkedUp: false,
|
|
265
|
+
configFound,
|
|
266
|
+
writeRootFollowedConfig: false,
|
|
267
|
+
});
|
|
268
|
+
|
|
246
269
|
if (typeof configName === 'string' && path.isAbsolute(configName)) {
|
|
247
270
|
const found = findNearestArkConfig(start, configName, opts);
|
|
248
|
-
|
|
249
|
-
const root = adoptWalkedRoot ? found.root : start;
|
|
250
|
-
return {
|
|
251
|
-
root,
|
|
252
|
-
writeRoot: start,
|
|
253
|
-
config: configName,
|
|
254
|
-
configPath: found.configPath,
|
|
255
|
-
configRoot: found.root,
|
|
256
|
-
walkedUp: found.walkedUp,
|
|
257
|
-
configFound: true,
|
|
258
|
-
writeRootFollowedConfig: adoptWalkedRoot && found.walkedUp,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
return {
|
|
262
|
-
root: start,
|
|
263
|
-
writeRoot: start,
|
|
264
|
-
config: configName,
|
|
265
|
-
configPath: configName,
|
|
266
|
-
configRoot: start,
|
|
267
|
-
walkedUp: false,
|
|
268
|
-
configFound: false,
|
|
269
|
-
writeRootFollowedConfig: false,
|
|
270
|
-
};
|
|
271
|
+
return found ? walkedResult(found) : startResult(configName, false);
|
|
271
272
|
}
|
|
272
273
|
|
|
273
274
|
const localPath = path.join(start, configName);
|
|
274
275
|
try {
|
|
275
276
|
if (fs.statSync(localPath, { throwIfNoEntry: false })?.isFile()) {
|
|
276
|
-
return
|
|
277
|
-
root: start,
|
|
278
|
-
writeRoot: start,
|
|
279
|
-
config: configName,
|
|
280
|
-
configPath: localPath,
|
|
281
|
-
configRoot: start,
|
|
282
|
-
walkedUp: false,
|
|
283
|
-
configFound: true,
|
|
284
|
-
writeRootFollowedConfig: false,
|
|
285
|
-
};
|
|
277
|
+
return startResult(localPath, true);
|
|
286
278
|
}
|
|
287
279
|
} catch {
|
|
288
280
|
// fall through to walk-up
|
|
289
281
|
}
|
|
290
282
|
|
|
291
283
|
const found = findNearestArkConfig(start, configName, opts);
|
|
292
|
-
|
|
293
|
-
const root = adoptWalkedRoot ? found.root : start;
|
|
294
|
-
return {
|
|
295
|
-
root,
|
|
296
|
-
writeRoot: start,
|
|
297
|
-
config: configName,
|
|
298
|
-
configPath: found.configPath,
|
|
299
|
-
configRoot: found.root,
|
|
300
|
-
walkedUp: found.walkedUp,
|
|
301
|
-
configFound: true,
|
|
302
|
-
writeRootFollowedConfig: adoptWalkedRoot && found.walkedUp,
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return {
|
|
307
|
-
root: start,
|
|
308
|
-
writeRoot: start,
|
|
309
|
-
config: configName,
|
|
310
|
-
configPath: localPath,
|
|
311
|
-
configRoot: start,
|
|
312
|
-
walkedUp: false,
|
|
313
|
-
configFound: false,
|
|
314
|
-
writeRootFollowedConfig: false,
|
|
315
|
-
};
|
|
284
|
+
return found ? walkedResult(found) : startResult(localPath, false);
|
|
316
285
|
}
|
|
317
286
|
|
|
318
287
|
/**
|