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.
Files changed (41) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +2 -2
  3. package/bin/ark-check-runtime.mjs +4 -2
  4. package/bin/ark-layer-match.mjs +606 -11
  5. package/bin/lib/analysis-engine.mjs +11 -11
  6. package/bin/lib/config-contract.mjs +492 -1
  7. package/bin/lib/diagnostic-catalog.mjs +32 -0
  8. package/bin/lib/doctor-advisories.mjs +60 -1
  9. package/bin/lib/doctor-plan.mjs +2 -2
  10. package/bin/lib/enforcement-honesty.mjs +22 -0
  11. package/bin/lib/flat-parent-pilot.mjs +252 -0
  12. package/bin/lib/html-report-advisories.mjs +2 -0
  13. package/bin/lib/html-report-depth.mjs +3 -1
  14. package/bin/lib/physical-cohesion.mjs +16 -18
  15. package/bin/lib/pilot-loop.mjs +21 -8
  16. package/bin/lib/team-parliament-io.mjs +4 -2
  17. package/dist/{configTypes-CTELo87-.d.ts → configTypes-Bbf_MFGe.d.ts} +36 -2
  18. package/dist/{diagnosticCatalog-WD2TPSwZ.d.ts → diagnosticCatalog-35mrwAHe.d.ts} +13 -3
  19. package/dist/eslint/index.cjs +4 -4
  20. package/dist/eslint/index.d.ts +51 -5
  21. package/dist/eslint/index.js +4 -4
  22. package/dist/index.cjs +35 -35
  23. package/dist/index.d.ts +104 -5
  24. package/dist/index.js +35 -35
  25. package/dist/nestjs/index.cjs +9 -9
  26. package/dist/nestjs/index.d.ts +3 -3
  27. package/dist/nestjs/index.js +9 -9
  28. package/dist/runtime/index.cjs +13 -13
  29. package/dist/runtime/index.d.ts +6 -6
  30. package/dist/runtime/index.js +13 -13
  31. package/dist/{types-yDNEQ603.d.ts → types-Bdj32QwA.d.ts} +1 -1
  32. package/dist/{types-fQ9-KrBj.d.ts → types-Ik_OakcI.d.ts} +1 -1
  33. package/docs/README.md +2 -1
  34. package/docs/agent-guide.md +9 -1
  35. package/docs/configuration.md +92 -1
  36. package/docs/diagnostics.md +32 -0
  37. package/docs/product-voice.md +1 -1
  38. package/package.json +1 -1
  39. package/schemas/ark.config.schema.json +124 -2
  40. package/server.json +2 -2
  41. package/templates/agent-skills/README.md +1 -1
@@ -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, sliceIdForPath } from '../ark-layer-match.mjs';
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
- function declaredSliceFolders(rules) {
139
- const out = [];
140
- const seen = new Set();
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
- const folders = Array.isArray(rule?.sliceFolders) ? rule.sliceFolders : [];
143
- for (const folder of folders) {
144
- if (typeof folder !== 'string' || folder.length === 0 || seen.has(folder.toLowerCase())) continue;
145
- seen.add(folder.toLowerCase());
146
- out.push(folder);
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 out;
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 sliceFolders = declaredSliceFolders(contract?.rules);
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, sliceFolders);
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
  }
@@ -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: reshape ? `reshape:${candidate.target}` : `pattern-b:${candidate.target}`,
249
- smellId: reshape ? 'physical-cohesion' : 'unknown',
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: reshape
266
- ? '/ark-loop (one reshape pilot) | re-doctor after pilot'
267
- : '/ark-autopilot (one cluster / one B pilot) | re-doctor after pilot',
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. Doctor builds this
276
- * list after advisories, so the loop never depends on call order.
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 vsBaseActive = violations.filter((_, index) => !baseKeys.has(occurrenceKeys[index]));
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: violations.filter((violation) => !activeViolations.includes(violation)),
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
- /** When `"deny"`, a declared shared root may not import a slice. */
71
- sharedImportsSlice?: 'deny';
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-fQ9-KrBj.js';
2
- import { A as ArkConfig, c as ArkConfigLoadResult } from './configTypes-CTELo87-.js';
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.22";
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.). */