arkgate 4.8.22 → 4.8.23
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 +23 -0
- package/README.md +2 -2
- package/bin/ark-check-runtime.mjs +4 -2
- package/bin/ark-layer-match.mjs +606 -11
- package/bin/lib/analysis-engine.mjs +11 -11
- package/bin/lib/config-contract.mjs +492 -1
- package/bin/lib/diagnostic-catalog.mjs +32 -0
- package/bin/lib/doctor-advisories.mjs +60 -1
- package/bin/lib/doctor-plan.mjs +2 -2
- package/bin/lib/enforcement-honesty.mjs +22 -0
- package/bin/lib/flat-parent-pilot.mjs +252 -0
- package/bin/lib/html-report-advisories.mjs +2 -0
- package/bin/lib/html-report-depth.mjs +3 -1
- package/bin/lib/physical-cohesion.mjs +16 -18
- package/bin/lib/pilot-loop.mjs +21 -8
- package/bin/lib/team-parliament-io.mjs +4 -2
- package/dist/{configTypes-CTELo87-.d.ts → configTypes-Bbf_MFGe.d.ts} +36 -2
- package/dist/{diagnosticCatalog-WD2TPSwZ.d.ts → diagnosticCatalog-35mrwAHe.d.ts} +13 -3
- package/dist/eslint/index.cjs +4 -4
- package/dist/eslint/index.d.ts +51 -5
- package/dist/eslint/index.js +4 -4
- package/dist/index.cjs +35 -35
- package/dist/index.d.ts +104 -5
- package/dist/index.js +35 -35
- package/dist/nestjs/index.cjs +9 -9
- package/dist/nestjs/index.d.ts +3 -3
- package/dist/nestjs/index.js +9 -9
- package/dist/runtime/index.cjs +13 -13
- package/dist/runtime/index.d.ts +6 -6
- package/dist/runtime/index.js +13 -13
- package/dist/{types-yDNEQ603.d.ts → types-Bdj32QwA.d.ts} +1 -1
- package/dist/{types-fQ9-KrBj.d.ts → types-Ik_OakcI.d.ts} +1 -1
- package/docs/README.md +2 -1
- package/docs/agent-guide.md +9 -1
- package/docs/configuration.md +92 -1
- package/docs/diagnostics.md +32 -0
- package/docs/product-voice.md +1 -1
- package/package.json +1 -1
- package/schemas/ark.config.schema.json +124 -2
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -21,10 +21,10 @@ import { collectStatesTransitionsResidual } from './states-transitions-presence.
|
|
|
21
21
|
import { collectStatusTransitionCatalogResidual } from './status-transition-catalog.mjs';
|
|
22
22
|
import { collectNoDomainFrontendResidual } from './no-domain-frontend.mjs';
|
|
23
23
|
import { collectInvariantCoverageResiduals } from './invariant-tests-path.mjs';
|
|
24
|
+
import { anyChildWallAdvisory, anySliceAlias } from '../ark-layer-match.mjs';
|
|
24
25
|
export { printAdrPresenceHint };
|
|
25
26
|
export { printDoctorCompactHuman, printDoctorDetailsHuman };
|
|
26
27
|
export { summarizeRulesUnderContract };
|
|
27
|
-
|
|
28
28
|
/** Optional S3 dual-match classifier when ark-shared exports it (soft dep for S5 landing). */
|
|
29
29
|
const matchingLayersForRelativePath =
|
|
30
30
|
typeof arkShared.matchingLayersForRelativePath === 'function'
|
|
@@ -694,7 +694,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
694
694
|
github: githubForBoundary,
|
|
695
695
|
adoptionStance: stanceFile,
|
|
696
696
|
stewardNudge: doctorAdvisories.stewardNudge,
|
|
697
|
-
physicalCohesionResidual: physicalCohesionResidualRemains(doctorAdvisories.physicalCohesion),
|
|
697
|
+
physicalCohesionResidual: physicalCohesionResidualRemains(doctorAdvisories.physicalCohesion), childWallAdvisory: anyChildWallAdvisory(config?.rules), sliceAliasDebt: anySliceAlias(config?.rules),
|
|
698
698
|
});
|
|
699
699
|
|
|
700
700
|
// Improvement compass: projection only — never feeds ok/valid/goal.met.
|
|
@@ -289,6 +289,8 @@ export function buildWritePathHonesty(activeHost, hardWriteActive = false, extra
|
|
|
289
289
|
* operatingMode?: string | null,
|
|
290
290
|
* activeBlockingViolations?: number | null,
|
|
291
291
|
* physicalCohesionResidual?: boolean,
|
|
292
|
+
* childWallAdvisory?: boolean,
|
|
293
|
+
* sliceAliasDebt?: boolean,
|
|
292
294
|
* }} input
|
|
293
295
|
*/
|
|
294
296
|
export function physicalCohesionResidualRemains(cohesion) {
|
|
@@ -489,6 +491,22 @@ export function buildProductHonesty(input = {}) {
|
|
|
489
491
|
// Informational only when no enforced arkrule plane — does not alone make unfinished.
|
|
490
492
|
}
|
|
491
493
|
|
|
494
|
+
if (input.childWallAdvisory === true) {
|
|
495
|
+
reasons.push({
|
|
496
|
+
id: 'child-wall-advisory',
|
|
497
|
+
message:
|
|
498
|
+
'A child wall lists siblings as advisory. Recorded crossings stay non-blocking, and a new sibling crossing past that baseline still fails. An enforce list makes those subtrees errors and does not finish the house. This house stays unfinished until siblings is deny.',
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (input.sliceAliasDebt === true) {
|
|
503
|
+
reasons.push({
|
|
504
|
+
id: 'slice-alias-debt',
|
|
505
|
+
message:
|
|
506
|
+
'Slice aliases are an owed move, not a destination. Aliased files are not finished until they move into the target slice folder.',
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
492
510
|
// EH05: environment residual deny-list (future reason ids stay architecture debt by default).
|
|
493
511
|
const ENVIRONMENT_REASON_IDS = new Set(['soft-write-host', 'native-fail-open']);
|
|
494
512
|
|
|
@@ -683,6 +701,8 @@ export function computeDoctorEnforcementHonesty({
|
|
|
683
701
|
nativeFailClosed,
|
|
684
702
|
nativeFailClosedPolicy,
|
|
685
703
|
physicalCohesionResidual,
|
|
704
|
+
childWallAdvisory,
|
|
705
|
+
sliceAliasDebt,
|
|
686
706
|
} = {}) {
|
|
687
707
|
const coverageHonesty = buildCoverageHonesty({
|
|
688
708
|
percent: governedPercent,
|
|
@@ -741,6 +761,8 @@ export function computeDoctorEnforcementHonesty({
|
|
|
741
761
|
emptyStewards,
|
|
742
762
|
stewardNudge,
|
|
743
763
|
physicalCohesionResidual,
|
|
764
|
+
childWallAdvisory,
|
|
765
|
+
sliceAliasDebt,
|
|
744
766
|
});
|
|
745
767
|
return {
|
|
746
768
|
coverageHonesty,
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor suggestion: a universe-level flat file that one child slice imports.
|
|
3
|
+
* Advisory only. Not a finding, not a verdict, not a config field.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import {
|
|
8
|
+
layerForRelativePath,
|
|
9
|
+
resolveGovernedSlice,
|
|
10
|
+
sliceAliasDestination,
|
|
11
|
+
sliceIdForPath,
|
|
12
|
+
} from '../ark-layer-match.mjs';
|
|
13
|
+
import { destinationKeepsLayerAndSlice } from './physical-cohesion.mjs';
|
|
14
|
+
|
|
15
|
+
const SPEC_MARKS = [' from "', " from '", ' from `', 'require("', "require('", 'import("', "import('"];
|
|
16
|
+
const EXT = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts'];
|
|
17
|
+
|
|
18
|
+
function ruleKey(rule) {
|
|
19
|
+
const child = rule?.childSlices;
|
|
20
|
+
if (!child?.sliceFolders?.length) return '';
|
|
21
|
+
return [
|
|
22
|
+
(rule.sliceFolders ?? []).join('\0'),
|
|
23
|
+
rule.sliceIdentity ?? '',
|
|
24
|
+
child.sliceFolders.join('\0'),
|
|
25
|
+
child.sliceIdentity ?? '',
|
|
26
|
+
(child.commonFolders ?? []).join('\0'),
|
|
27
|
+
].join('\n');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function distinctChildRules(rules) {
|
|
31
|
+
const out = [];
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
for (const rule of Array.isArray(rules) ? rules : []) {
|
|
34
|
+
const key = ruleKey(rule);
|
|
35
|
+
if (!key || seen.has(key)) continue;
|
|
36
|
+
seen.add(key);
|
|
37
|
+
out.push(rule);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isFlatParent(rel, rule) {
|
|
43
|
+
const universe = sliceIdForPath(rel, rule.sliceFolders, rule.sliceIdentity);
|
|
44
|
+
const raw = sliceIdForPath(rel, rule.childSlices.sliceFolders, rule.childSlices.sliceIdentity);
|
|
45
|
+
return Boolean(universe && raw && raw.toLowerCase() === universe.toLowerCase());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function withoutComments(text) {
|
|
49
|
+
let out = '';
|
|
50
|
+
let i = 0;
|
|
51
|
+
while (i < text.length) {
|
|
52
|
+
const c = text[i];
|
|
53
|
+
const n = text[i + 1];
|
|
54
|
+
if (c === '/' && n === '/') {
|
|
55
|
+
while (i < text.length && text[i] !== '\n') i += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (c === '/' && n === '*') {
|
|
59
|
+
i += 2;
|
|
60
|
+
while (i + 1 < text.length && !(text[i] === '*' && text[i + 1] === '/')) i += 1;
|
|
61
|
+
i = Math.min(text.length, i + 2);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
out += c;
|
|
65
|
+
i += 1;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function relativeSpecifiers(text) {
|
|
71
|
+
const specs = [];
|
|
72
|
+
const source = withoutComments(String(text));
|
|
73
|
+
let i = 0;
|
|
74
|
+
while (i < source.length) {
|
|
75
|
+
let at = -1;
|
|
76
|
+
let mark = '';
|
|
77
|
+
for (const candidate of SPEC_MARKS) {
|
|
78
|
+
const found = source.indexOf(candidate, i);
|
|
79
|
+
if (found !== -1 && (at === -1 || found < at)) {
|
|
80
|
+
at = found;
|
|
81
|
+
mark = candidate;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (at === -1) break;
|
|
85
|
+
const quote = mark[mark.length - 1];
|
|
86
|
+
const start = at + mark.length;
|
|
87
|
+
const end = source.indexOf(quote, start);
|
|
88
|
+
if (end === -1) break;
|
|
89
|
+
const spec = source.slice(start, end);
|
|
90
|
+
if (spec.startsWith('.')) specs.push(spec);
|
|
91
|
+
i = end + 1;
|
|
92
|
+
}
|
|
93
|
+
return specs;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveRelative(fromFile, spec, fileSet) {
|
|
97
|
+
const base = fromFile.split('/');
|
|
98
|
+
base.pop();
|
|
99
|
+
for (const part of spec.split('/')) {
|
|
100
|
+
if (part === '' || part === '.') continue;
|
|
101
|
+
if (part === '..') {
|
|
102
|
+
if (base.length === 0) return null;
|
|
103
|
+
base.pop();
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
base.push(part);
|
|
107
|
+
}
|
|
108
|
+
const joined = base.join('/');
|
|
109
|
+
const names = [joined];
|
|
110
|
+
for (const ext of EXT) names.push(`${joined}${ext}`, `${joined}/index${ext}`);
|
|
111
|
+
for (const name of names) {
|
|
112
|
+
if (fileSet.has(name)) return name;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readText(root, rel) {
|
|
118
|
+
try {
|
|
119
|
+
return fs.readFileSync(path.join(root, rel), 'utf8');
|
|
120
|
+
} catch {
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function childImporters(flatFiles, files, rule, root) {
|
|
126
|
+
const flat = new Set(flatFiles);
|
|
127
|
+
const known = new Set(files);
|
|
128
|
+
const importers = new Map();
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
const place = resolveGovernedSlice(file, rule);
|
|
131
|
+
if (!place.childId) continue;
|
|
132
|
+
for (const spec of relativeSpecifiers(readText(root, file))) {
|
|
133
|
+
const target = resolveRelative(file, spec, known);
|
|
134
|
+
if (!target || !flat.has(target) || target === file) continue;
|
|
135
|
+
let ids = importers.get(target);
|
|
136
|
+
if (!ids) {
|
|
137
|
+
ids = new Set();
|
|
138
|
+
importers.set(target, ids);
|
|
139
|
+
}
|
|
140
|
+
ids.add(place.childId);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return importers;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function basename(rel) {
|
|
147
|
+
const parts = rel.split('/');
|
|
148
|
+
return parts[parts.length - 1] ?? rel;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {{ root: string, files: string[], rules: object[] | undefined, layers: object[] | undefined }} input
|
|
153
|
+
* @returns {object | null}
|
|
154
|
+
*/
|
|
155
|
+
export function collectFlatParentPilot(input) {
|
|
156
|
+
const rules = distinctChildRules(input?.rules);
|
|
157
|
+
if (rules.length === 0) return null;
|
|
158
|
+
const files = (Array.isArray(input.files) ? input.files : [])
|
|
159
|
+
.map((file) => String(file).replace(/\\/g, '/'))
|
|
160
|
+
.filter((file) => file.length > 0);
|
|
161
|
+
const layers = input.layers;
|
|
162
|
+
const moves = [];
|
|
163
|
+
const seen = new Set();
|
|
164
|
+
for (const rule of rules) {
|
|
165
|
+
const flat = files.filter((file) => isFlatParent(file, rule));
|
|
166
|
+
if (flat.length === 0) continue;
|
|
167
|
+
const importers = childImporters(flat, files, rule, input.root);
|
|
168
|
+
for (const file of flat) {
|
|
169
|
+
if (seen.has(file)) continue;
|
|
170
|
+
const ids = importers.get(file);
|
|
171
|
+
if (!ids || ids.size !== 1) continue;
|
|
172
|
+
const importer = [...ids][0];
|
|
173
|
+
const destination = sliceAliasDestination(file, importer, rule.childSlices);
|
|
174
|
+
if (!destination) continue;
|
|
175
|
+
const destFile = `${destination}/${basename(file)}`;
|
|
176
|
+
const layer = layerForRelativePath(file, layers);
|
|
177
|
+
if (!layer || layer !== layerForRelativePath(destFile, layers)) continue;
|
|
178
|
+
const toPlace = resolveGovernedSlice(destFile, rule);
|
|
179
|
+
if ((toPlace.childId ?? '') !== importer) continue;
|
|
180
|
+
const fromPlace = resolveGovernedSlice(file, rule);
|
|
181
|
+
const keepsSlice = destinationKeepsLayerAndSlice(file, destFile, layers, [rule]);
|
|
182
|
+
const wallSame =
|
|
183
|
+
(fromPlace.universeId ?? '') === (toPlace.universeId ?? '') &&
|
|
184
|
+
(fromPlace.childId ?? '') === (toPlace.childId ?? '');
|
|
185
|
+
seen.add(file);
|
|
186
|
+
moves.push({
|
|
187
|
+
file,
|
|
188
|
+
importer,
|
|
189
|
+
destination,
|
|
190
|
+
layer,
|
|
191
|
+
keepsLayer: true,
|
|
192
|
+
keepsSlice,
|
|
193
|
+
destinationChild: toPlace.childId,
|
|
194
|
+
agreesWithWall: keepsSlice === wallSame,
|
|
195
|
+
evidence: `One child imports ${file} (${importer}). Move it into ${destination}. The ${layer} layer stays. This is a suggestion with that importer as its evidence.`,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (moves.length === 0) return null;
|
|
200
|
+
moves.sort((left, right) => (left.file < right.file ? -1 : left.file > right.file ? 1 : 0));
|
|
201
|
+
return {
|
|
202
|
+
advisory: true,
|
|
203
|
+
notAScore: true,
|
|
204
|
+
proposed: true,
|
|
205
|
+
applied: false,
|
|
206
|
+
neverMechanicalSafe: true,
|
|
207
|
+
moves,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** One pilot candidate per move. Appended after pattern bets and the reshape card. */
|
|
212
|
+
export function flatParentCandidates(section) {
|
|
213
|
+
if (!section || !Array.isArray(section.moves)) return [];
|
|
214
|
+
return section.moves
|
|
215
|
+
.filter((move) => move && typeof move.file === 'string' && move.file.length > 0)
|
|
216
|
+
.map((move) => ({
|
|
217
|
+
source: 'flat-parent',
|
|
218
|
+
target: move.file,
|
|
219
|
+
move: typeof move.evidence === 'string' ? move.evidence : `Move ${move.file} into its one importing child. This is a suggestion.`,
|
|
220
|
+
moveSample: [{ from: move.file, to: `${move.destination}/${basename(move.file)}` }],
|
|
221
|
+
successSignal: 're-run doctor: this file is no longer a single-importer flat parent',
|
|
222
|
+
killSwitch: 'leave the file where it is',
|
|
223
|
+
doNot: [
|
|
224
|
+
'a suggestion with one importer as its evidence',
|
|
225
|
+
'leave a file in place when two children import it',
|
|
226
|
+
'one pilot at a time; re-doctor before the next card',
|
|
227
|
+
'never weaken the contract to make a move pass',
|
|
228
|
+
],
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function printFlatParentPilot(section, io) {
|
|
233
|
+
const moves = Array.isArray(section?.moves) ? section.moves : [];
|
|
234
|
+
if (moves.length === 0) return;
|
|
235
|
+
console.log('');
|
|
236
|
+
console.log(io.color.bold('Flat parent files (suggestion)'));
|
|
237
|
+
for (const move of moves.slice(0, 5)) io.line(io.warn, move.evidence);
|
|
238
|
+
if (moves.length > 5) io.line(' ', io.color.dim(`…(+${moves.length - 5} more in doctor JSON)`));
|
|
239
|
+
io.line(' ', io.color.dim('suggestion only — one importer is the evidence; the gate verdict is unchanged'));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function flatParentPilotHtml(section, escape) {
|
|
243
|
+
const moves = Array.isArray(section?.moves) ? section.moves : [];
|
|
244
|
+
if (moves.length === 0) return '';
|
|
245
|
+
const esc = typeof escape === 'function' ? escape : (value) => String(value);
|
|
246
|
+
const items = moves
|
|
247
|
+
.slice(0, 5)
|
|
248
|
+
.map((move) => `<li>${esc(move.evidence ?? '')}</li>`)
|
|
249
|
+
.join('');
|
|
250
|
+
const more = moves.length > 5 ? `<p class="muted">…(+${moves.length - 5} more in doctor JSON)</p>` : '';
|
|
251
|
+
return `<section class="section card" data-advisory="flatParentPilot"><h2>Flat parent files <span class="muted">(suggestion — one importer is the evidence; not a score; the verdict is unchanged)</span></h2><ul>${items}</ul>${more}</section>`;
|
|
252
|
+
}
|
|
@@ -13,6 +13,7 @@ import { formatRulesUnderContractHtml } from './rules-under-contract.mjs';
|
|
|
13
13
|
import { formatArkRunHtml } from './ark-run-report.mjs';
|
|
14
14
|
import { formatArkOrderHtml } from './ark-order-report.mjs';
|
|
15
15
|
import { primaryImprovementCompassNextAction } from './improvement-compass.mjs';
|
|
16
|
+
import { flatParentPilotHtml } from './flat-parent-pilot.mjs';
|
|
16
17
|
|
|
17
18
|
// htmlEscape is injected by the caller (html-report.mjs) — importing it back
|
|
18
19
|
// would be a dependency cycle, and the repo's own gate blocks that. The
|
|
@@ -355,6 +356,7 @@ export function renderAdvisorySections(advisories, escape) {
|
|
|
355
356
|
deepModuleCoachHtml(advisories.deepModuleCoach),
|
|
356
357
|
stewardNudgeHtml(advisories.stewardNudge),
|
|
357
358
|
sliceIdentityHtml(advisories.sliceIdentity),
|
|
359
|
+
flatParentPilotHtml(advisories.flatParentPilot, esc),
|
|
358
360
|
contractHealthHtml(advisories.contractHealth),
|
|
359
361
|
ambientStateHtml(advisories.ambientState),
|
|
360
362
|
physicalCohesionHtml(advisories.physicalCohesion),
|
|
@@ -28,6 +28,7 @@ import { buildDeepModuleCoachAdvisory } from './deep-module-coach.mjs';
|
|
|
28
28
|
import { computePhysicalCohesion } from './physical-cohesion.mjs';
|
|
29
29
|
import { computeReshapeDecisionMemory } from './reshape-decisions.mjs';
|
|
30
30
|
import { POST_GREEN_LEDE, operatingModeTitle } from './product-copy.mjs';
|
|
31
|
+
import { anyChildWallAdvisory, anySliceAlias } from '../ark-layer-match.mjs';
|
|
31
32
|
|
|
32
33
|
function esc(value) {
|
|
33
34
|
return String(value)
|
|
@@ -174,7 +175,8 @@ export function buildReportDepthPayload(
|
|
|
174
175
|
: null,
|
|
175
176
|
primaryNextAction: postGreenPath?.action ?? dualTruthNext,
|
|
176
177
|
activeBlockingViolations: activeBlockingCount,
|
|
177
|
-
physicalCohesionResidual: physicalCohesionResidualRemains(physicalCohesion),
|
|
178
|
+
physicalCohesionResidual: physicalCohesionResidualRemains(physicalCohesion), childWallAdvisory: anyChildWallAdvisory(config?.rules),
|
|
179
|
+
sliceAliasDebt: anySliceAlias(config?.rules),
|
|
178
180
|
});
|
|
179
181
|
// Doctor parity: same physical-cohesion + baseline stale facts as runDoctor.
|
|
180
182
|
const baselineStale =
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* as W01 layer roles: a miss costs a warning line, never a verdict.
|
|
16
16
|
*/
|
|
17
17
|
import path from 'node:path';
|
|
18
|
-
import { layerForRelativePath,
|
|
18
|
+
import { layerForRelativePath, resolveGovernedSlice } from '../ark-layer-match.mjs';
|
|
19
19
|
|
|
20
20
|
/** ADR 0010 D3 — corpus-calibrated, fixed. */
|
|
21
21
|
const CLUSTER_MIN = 40;
|
|
@@ -135,23 +135,21 @@ function anchorIsConsolidationSubtree(anchor, concept) {
|
|
|
135
135
|
return String(anchor).split('/').some((part) => part === concept);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Layer must match. Universe id and child id must match on every rule, using
|
|
140
|
+
* the same resolver as the walls (`sliceIdentity` included). A missing child
|
|
141
|
+
* id is universe common. Two children of one universe do not keep the slice.
|
|
142
|
+
*/
|
|
143
|
+
export function destinationKeepsLayerAndSlice(fromRel, toRel, layers, rules) {
|
|
144
|
+
if (layerForRelativePath(fromRel, layers) !== layerForRelativePath(toRel, layers)) return false;
|
|
141
145
|
for (const rule of Array.isArray(rules) ? rules : []) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
146
|
+
if (!rule || typeof rule !== 'object') continue;
|
|
147
|
+
const from = resolveGovernedSlice(fromRel, rule);
|
|
148
|
+
const to = resolveGovernedSlice(toRel, rule);
|
|
149
|
+
if ((from.universeId ?? '') !== (to.universeId ?? '')) return false;
|
|
150
|
+
if ((from.childId ?? '') !== (to.childId ?? '')) return false;
|
|
148
151
|
}
|
|
149
|
-
return
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function destinationKeepsLayerAndSlice(fromRel, toRel, layers, sliceFolders) {
|
|
153
|
-
return layerForRelativePath(fromRel, layers) === layerForRelativePath(toRel, layers)
|
|
154
|
-
&& sliceIdForPath(fromRel, sliceFolders) === sliceIdForPath(toRel, sliceFolders);
|
|
152
|
+
return true;
|
|
155
153
|
}
|
|
156
154
|
|
|
157
155
|
function withheldPilot(concept, note) {
|
|
@@ -168,7 +166,7 @@ export function computeReshapePilot(cohesion, files, root, contract) {
|
|
|
168
166
|
const top = cohesion?.findings?.[0];
|
|
169
167
|
if (!top) return null;
|
|
170
168
|
const layers = contract?.layers;
|
|
171
|
-
const
|
|
169
|
+
const rules = Array.isArray(contract?.rules) ? contract.rules : [];
|
|
172
170
|
// Full anchor map: the finding's anchors are display-filtered (>= MIRROR_MIN).
|
|
173
171
|
const byAnchor = new Map();
|
|
174
172
|
const relOf = (abs) => path.relative(root, abs).split(path.sep).join('/');
|
|
@@ -193,7 +191,7 @@ export function computeReshapePilot(cohesion, files, root, contract) {
|
|
|
193
191
|
const ranked = classifyPhysical(rel);
|
|
194
192
|
if (!ranked || ranked.concept !== top.concept || ranked.anchor !== anchor.path) return false;
|
|
195
193
|
const to = `${targetDir}/${rel.split('/').at(-1)}`;
|
|
196
|
-
return destinationKeepsLayerAndSlice(rel, to, layers,
|
|
194
|
+
return destinationKeepsLayerAndSlice(rel, to, layers, rules);
|
|
197
195
|
}).sort().map((rel) => ({ from: rel, to: `${targetDir}/${rel.split('/').at(-1)}` }));
|
|
198
196
|
if (legal.length > 0) legalAnchors.push({ ...anchor, targetDir, legal });
|
|
199
197
|
}
|
package/bin/lib/pilot-loop.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
buildPatternBetsFromSmells,
|
|
10
10
|
isNonProductionPilotPath,
|
|
11
11
|
} from './design-smells.mjs';
|
|
12
|
+
import { flatParentCandidates } from './flat-parent-pilot.mjs';
|
|
12
13
|
|
|
13
14
|
/** Stable product id for JSON / tests. */
|
|
14
15
|
export const PILOT_LOOP_ID = 'one-pilot-redoctor';
|
|
@@ -181,7 +182,7 @@ export function formatExtractionCard(card) {
|
|
|
181
182
|
* One proposed pilot. The loop does not know designWeak or which sensor
|
|
182
183
|
* produced the card — only this list.
|
|
183
184
|
*
|
|
184
|
-
* @typedef {'pattern-bet' | 'reshape'} PilotSource
|
|
185
|
+
* @typedef {'pattern-bet' | 'reshape' | 'flat-parent'} PilotSource
|
|
185
186
|
* @typedef {{
|
|
186
187
|
* source: PilotSource,
|
|
187
188
|
* target: string,
|
|
@@ -243,10 +244,15 @@ function extractionCardFromCandidate(candidate) {
|
|
|
243
244
|
.filter((entry) => typeof entry === 'string' && entry.length > 0)
|
|
244
245
|
.slice(0, 8);
|
|
245
246
|
const reshape = candidate.source === 'reshape';
|
|
247
|
+
const flatParent = candidate.source === 'flat-parent';
|
|
246
248
|
return {
|
|
247
249
|
id: PILOT_LOOP_ID,
|
|
248
|
-
patternBetId:
|
|
249
|
-
|
|
250
|
+
patternBetId: flatParent
|
|
251
|
+
? `flat-parent:${candidate.target}`
|
|
252
|
+
: reshape
|
|
253
|
+
? `reshape:${candidate.target}`
|
|
254
|
+
: `pattern-b:${candidate.target}`,
|
|
255
|
+
smellId: flatParent ? 'flat-parent' : reshape ? 'physical-cohesion' : 'unknown',
|
|
250
256
|
pilot: candidate.target,
|
|
251
257
|
pilotTarget: candidate.target,
|
|
252
258
|
evidence,
|
|
@@ -262,9 +268,11 @@ function extractionCardFromCandidate(candidate) {
|
|
|
262
268
|
loopStep: 'one-pilot',
|
|
263
269
|
reDoctor: 'ark-check --doctor --json',
|
|
264
270
|
rePlan: 'ark-check --plan --json',
|
|
265
|
-
next:
|
|
266
|
-
? '/ark-loop (one
|
|
267
|
-
:
|
|
271
|
+
next: flatParent
|
|
272
|
+
? '/ark-loop (one flat-parent suggestion) | re-doctor after the move'
|
|
273
|
+
: reshape
|
|
274
|
+
? '/ark-loop (one reshape pilot) | re-doctor after pilot'
|
|
275
|
+
: '/ark-autopilot (one cluster / one B pilot) | re-doctor after pilot',
|
|
268
276
|
};
|
|
269
277
|
}
|
|
270
278
|
|
|
@@ -272,8 +280,10 @@ function extractionCardFromCandidate(candidate) {
|
|
|
272
280
|
* Normalize design-weak pattern bets and a proposed reshape pilot into one list.
|
|
273
281
|
* Pattern bets stay in selector rank (best first) and are omitted unless
|
|
274
282
|
* design fitness is design-weak, so that path keeps today's card. A reshape
|
|
275
|
-
* card is appended when physical cohesion proposes one.
|
|
276
|
-
*
|
|
283
|
+
* card is appended when physical cohesion proposes one. A flat-parent move
|
|
284
|
+
* is appended after that: one file with one importer waits behind a card the
|
|
285
|
+
* loop already promised. Doctor builds this list after advisories, so the
|
|
286
|
+
* loop never depends on call order.
|
|
277
287
|
*
|
|
278
288
|
* @param {{
|
|
279
289
|
* designWeak?: boolean,
|
|
@@ -317,6 +327,9 @@ export function collectPilotCandidates(designFitness = {}, advisories = {}) {
|
|
|
317
327
|
...(Array.isArray(card.doNot) ? { doNot: card.doNot } : {}),
|
|
318
328
|
});
|
|
319
329
|
}
|
|
330
|
+
for (const candidate of flatParentCandidates(advisories?.flatParentPilot)) {
|
|
331
|
+
candidates.push(candidate);
|
|
332
|
+
}
|
|
320
333
|
return candidates;
|
|
321
334
|
}
|
|
322
335
|
|
|
@@ -6,6 +6,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { createHash } from 'node:crypto';
|
|
9
|
+
import { applyAdvisorySiblingRatchet } from '../ark-layer-match.mjs';
|
|
9
10
|
import {
|
|
10
11
|
baselineKeysFromDocument,
|
|
11
12
|
classifyBaselineKeyDelta,
|
|
@@ -321,7 +322,8 @@ export function applyAgainstRatchet({
|
|
|
321
322
|
}) {
|
|
322
323
|
const baseRaw = readJsonMaybe(gitShowText(root, againstRef, '.ark-baseline.json'));
|
|
323
324
|
const baseKeys = new Set(baselineKeysFromDocument(baseRaw));
|
|
324
|
-
const
|
|
325
|
+
const judged = baseRaw ? applyAdvisorySiblingRatchet(violations, occurrenceKeys, baseKeys) : violations;
|
|
326
|
+
const vsBaseActive = judged.filter((_, index) => !baseKeys.has(occurrenceKeys[index]));
|
|
325
327
|
const changedSet = new Set(changedPaths ?? []);
|
|
326
328
|
const activeViolations = changed
|
|
327
329
|
? vsBaseActive.filter((violation) =>
|
|
@@ -330,7 +332,7 @@ export function applyAgainstRatchet({
|
|
|
330
332
|
: vsBaseActive;
|
|
331
333
|
return {
|
|
332
334
|
activeViolations,
|
|
333
|
-
suppressed:
|
|
335
|
+
suppressed: judged.filter((violation) => !activeViolations.includes(violation)),
|
|
334
336
|
};
|
|
335
337
|
}
|
|
336
338
|
|
|
@@ -67,8 +67,42 @@ type ArkConfigRule = {
|
|
|
67
67
|
sharedRoots?: string[];
|
|
68
68
|
/** Directed slice→slice edges the repo declares on purpose. */
|
|
69
69
|
allowedCrossSlice?: ArkConfigCrossSliceEdge[];
|
|
70
|
-
/**
|
|
71
|
-
|
|
70
|
+
/**
|
|
71
|
+
* `"deny"` blocks a shared root from importing a slice.
|
|
72
|
+
* `"deny-cross-parent"` leaves that hop allowed and asks the whole-graph
|
|
73
|
+
* check to report a slice that reaches another universe through shared.
|
|
74
|
+
*/
|
|
75
|
+
sharedImportsSlice?: 'deny' | 'deny-cross-parent';
|
|
76
|
+
/**
|
|
77
|
+
* Optional inner wall. Absent keeps today's universe-wall output.
|
|
78
|
+
* `siblings` defaults to deny. An object keeps that default and lists enforced subtrees.
|
|
79
|
+
* `parentMayImportChild` defaults to false.
|
|
80
|
+
*/
|
|
81
|
+
childSlices?: ArkConfigChildSlices;
|
|
82
|
+
};
|
|
83
|
+
type ArkConfigChildSlices = {
|
|
84
|
+
sliceFolders: string[];
|
|
85
|
+
sliceIdentity?: ArkConfigSliceIdentity;
|
|
86
|
+
commonFolders?: string[];
|
|
87
|
+
siblings?: 'deny' | 'advisory' | {
|
|
88
|
+
default: 'deny' | 'advisory';
|
|
89
|
+
enforce?: string[];
|
|
90
|
+
};
|
|
91
|
+
parentMayImportChild?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Directed child-slice allowances. `*` is one whole path segment.
|
|
94
|
+
* Clears a sibling crossing only. The universe `allowedCrossSlice` does not read this list.
|
|
95
|
+
*/
|
|
96
|
+
allowedCrossSlice?: ArkConfigCrossSliceEdge[];
|
|
97
|
+
/**
|
|
98
|
+
* Files outside the slice trees, borrowed onto a child id until they move.
|
|
99
|
+
* `to` is the universe id plus one child segment.
|
|
100
|
+
*/
|
|
101
|
+
sliceAliases?: ArkConfigSliceAlias[];
|
|
102
|
+
};
|
|
103
|
+
type ArkConfigSliceAlias = {
|
|
104
|
+
from: string;
|
|
105
|
+
to: string;
|
|
72
106
|
};
|
|
73
107
|
type ArkConfigCrossSliceEdge = {
|
|
74
108
|
from: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { e as CreateArchitectureProfileOptions, b as ArchitectureProfile, d as ArkCheckConfig, C as CreateArchitectureProfileFromArkConfigOptions, f as CreateElevenLayerArkConfigOptions, i as Policy, j as IntentCreator, I as IntentName } from './types-
|
|
2
|
-
import { A as ArkConfig, c as ArkConfigLoadResult } from './configTypes-
|
|
1
|
+
import { e as CreateArchitectureProfileOptions, b as ArchitectureProfile, d as ArkCheckConfig, C as CreateArchitectureProfileFromArkConfigOptions, f as CreateElevenLayerArkConfigOptions, i as Policy, j as IntentCreator, I as IntentName } from './types-Ik_OakcI.js';
|
|
2
|
+
import { A as ArkConfig, c as ArkConfigLoadResult } from './configTypes-Bbf_MFGe.js';
|
|
3
3
|
|
|
4
4
|
/** Versioned public result contract types + JSON Schema (adapter envelope). */
|
|
5
5
|
/**
|
|
@@ -38,6 +38,12 @@ type AdapterViolationInput = {
|
|
|
38
38
|
severity?: unknown;
|
|
39
39
|
/** When false, finding is non-blocking (e.g. type-only placement debt). */
|
|
40
40
|
failsStrict?: unknown;
|
|
41
|
+
/** Nested-wall reason. The ruleId stays LAYER_IMPORT_VIOLATION. Not part of the baseline key. */
|
|
42
|
+
reasonId?: unknown;
|
|
43
|
+
/** Last segment of the importer universe id, when the finding has a reasonId. */
|
|
44
|
+
universeFrom?: unknown;
|
|
45
|
+
/** Last segment of the importee universe id, when the finding has a reasonId. */
|
|
46
|
+
universeTo?: unknown;
|
|
41
47
|
nextAction?: unknown;
|
|
42
48
|
/** U04: the denied capability id on CAPABILITY_VIOLATION. */
|
|
43
49
|
capability?: unknown;
|
|
@@ -392,7 +398,7 @@ declare function createAdapterResult(input: {
|
|
|
392
398
|
}): CurrentAdapterResult;
|
|
393
399
|
|
|
394
400
|
/** ArkGate library version — single source of truth. */
|
|
395
|
-
declare const version = "4.8.
|
|
401
|
+
declare const version = "4.8.23";
|
|
396
402
|
|
|
397
403
|
/**
|
|
398
404
|
* AI Code Gate (basic).
|
|
@@ -412,6 +418,10 @@ interface AICodeGateViolation {
|
|
|
412
418
|
target?: string;
|
|
413
419
|
fromLayer?: string;
|
|
414
420
|
toLayer?: string;
|
|
421
|
+
/** Nested-wall reason. Absent on a classic layer deny and on a child-only parent import. */
|
|
422
|
+
reasonId?: string;
|
|
423
|
+
/** False for an advisory sibling crossing. Omitted findings still block the snippet. */
|
|
424
|
+
failsStrict?: boolean;
|
|
415
425
|
details?: unknown;
|
|
416
426
|
}
|
|
417
427
|
/** Extension point for external analyzers (AST, semantic, etc.). */
|