arkgate 4.6.4 → 4.6.6
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 +77 -2105
- package/README.md +11 -9
- package/bin/ark-check-runtime.mjs +136 -16
- package/bin/ark-mcp-runtime.mjs +18 -30
- package/bin/ark.mjs +13 -3
- package/bin/lib/adapter-contract.mjs +13 -9
- package/bin/lib/adoption-stance.mjs +104 -0
- package/bin/lib/agent-projection-command.mjs +18 -0
- package/bin/lib/agent-projection.mjs +2 -2
- package/bin/lib/analysis-engine.mjs +5 -5
- package/bin/lib/ci-and-commands.mjs +3 -3
- package/bin/lib/ci-merge-boundary.mjs +91 -0
- package/bin/lib/config-contract.mjs +2 -0
- package/bin/lib/design-delta.mjs +2 -2
- package/bin/lib/design-smells.mjs +1 -1
- package/bin/lib/diagnostic-catalog.mjs +6 -5
- package/bin/lib/doctor-advisories.mjs +2 -2
- package/bin/lib/doctor-next-actions.mjs +35 -5
- package/bin/lib/doctor-plan.mjs +164 -133
- package/bin/lib/enforcement-honesty.mjs +72 -0
- package/bin/lib/first-run-help.mjs +8 -7
- package/bin/lib/graph-blind.mjs +15 -6
- package/bin/lib/html-report-advisories.mjs +10 -2
- package/bin/lib/html-report.mjs +2 -2
- package/bin/lib/install-migrate.mjs +10 -0
- package/bin/lib/invariant-coverage.mjs +6 -2
- package/bin/lib/managed-upgrade.mjs +8 -3
- package/bin/lib/mcp-adoption.mjs +19 -0
- package/bin/lib/policy-delta-io.mjs +1 -1
- package/bin/lib/post-green-path.mjs +5 -1
- package/bin/lib/presets.mjs +22 -0
- package/bin/lib/product-copy.mjs +6 -3
- package/bin/lib/remediation.mjs +74 -10
- package/bin/lib/skill-install.mjs +2 -0
- package/bin/lib/snippet-analysis.mjs +40 -8
- package/bin/lib/start-preview.mjs +12 -22
- package/bin/lib/status-command.mjs +16 -0
- package/bin/lib/status-manifest.mjs +8 -2
- package/bin/lib/team-parliament-io.mjs +62 -2
- package/bin/lib/team-parliament.mjs +25 -5
- package/bin/lib/unavailable-analysis.mjs +1 -0
- package/dist/{configTypes-B8uIcLaG.d.ts → configTypes-l6XiwiC1.d.ts} +7 -0
- package/dist/eslint/index.cjs +3 -3
- package/dist/eslint/index.d.ts +1 -1
- package/dist/eslint/index.js +3 -3
- package/dist/index.cjs +26 -26
- package/dist/index.d.ts +20 -3
- package/dist/index.js +29 -29
- package/docs/README.md +6 -9
- package/docs/agent-guide.md +10 -0
- package/docs/ai-gates.md +12 -5
- package/docs/brownfield-adoption.md +7 -1
- package/docs/configuration.md +11 -2
- package/docs/develop.md +4 -2
- package/docs/diagnostics.md +17 -7
- package/docs/package-surface.md +6 -4
- package/docs/product-voice.md +6 -4
- package/docs/threat-model.md +2 -2
- package/docs/use.md +5 -4
- package/package.json +1 -1
- package/schemas/ark.config.schema.json +6 -0
- package/schemas/ark.design-delta.schema.json +1 -1
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +7 -0
- package/templates/agent-skills/ark-explore/SKILL.md +6 -0
- package/templates/agent-skills/ark-place/SKILL.md +11 -4
- package/templates/agent-skills/ark-upgrade/SKILL.md +9 -2
- package/templates/skills/ark-adopt.md +7 -0
- package/templates/skills/ark-explore.md +6 -0
- package/templates/skills/ark-place.md +11 -4
- package/templates/skills/ark-upgrade.md +9 -2
package/bin/lib/graph-blind.mjs
CHANGED
|
@@ -16,8 +16,16 @@ import { normalize } from './scan-files.mjs';
|
|
|
16
16
|
|
|
17
17
|
const MAX_LIST = 8;
|
|
18
18
|
const MAX_FILE_BYTES = 256 * 1024;
|
|
19
|
-
/**
|
|
20
|
-
const
|
|
19
|
+
/** Floor for tiny trees. Large included Next.js trees must still scan. */
|
|
20
|
+
const GRAPH_SCAN_FLOOR = 2500;
|
|
21
|
+
/** Hard cap so a 10k fixture cannot blow the doctor resident warm UX ceiling. */
|
|
22
|
+
const GRAPH_SCAN_HARD_CAP = 8000;
|
|
23
|
+
|
|
24
|
+
/** Threshold scales with the included tree — never a hard 2500 that defers a 3300-file app. */
|
|
25
|
+
export function graphScanLimit(includedCount) {
|
|
26
|
+
const count = Number(includedCount) || 0;
|
|
27
|
+
return Math.min(GRAPH_SCAN_HARD_CAP, Math.max(GRAPH_SCAN_FLOOR, count));
|
|
28
|
+
}
|
|
21
29
|
const LEXICAL_GATE = /\b(?:import|require)\s*\(|\bimport\s+\w+\s*=\s*require\s*\(/;
|
|
22
30
|
|
|
23
31
|
/**
|
|
@@ -123,9 +131,9 @@ export function detectGraphBlindSpots(ts, root, files = []) {
|
|
|
123
131
|
};
|
|
124
132
|
}
|
|
125
133
|
|
|
126
|
-
// Large-tree deferral:
|
|
127
|
-
|
|
128
|
-
if (files.length >
|
|
134
|
+
// Large-tree deferral: only above the proportional cap (included count, floor 2500, cap 8000).
|
|
135
|
+
const scanLimit = graphScanLimit(files.length);
|
|
136
|
+
if (files.length > scanLimit) {
|
|
129
137
|
return {
|
|
130
138
|
available: true,
|
|
131
139
|
advisory: true,
|
|
@@ -136,7 +144,7 @@ export function detectGraphBlindSpots(ts, root, files = []) {
|
|
|
136
144
|
otherNonLiteralCount: 0,
|
|
137
145
|
truncated: 0,
|
|
138
146
|
edges: [],
|
|
139
|
-
note: `Graph-blind full scan deferred (${files.length} files > ${
|
|
147
|
+
note: `Graph-blind full scan deferred (${files.length} files > proportional limit ${scanLimit}). Non-literal dynamic import/require remain unresolvable in architecture analysis; advisory incomplete-graph honesty is not enumerated at this scale.`,
|
|
140
148
|
};
|
|
141
149
|
}
|
|
142
150
|
|
|
@@ -177,6 +185,7 @@ export function detectGraphBlindSpots(ts, root, files = []) {
|
|
|
177
185
|
available: true,
|
|
178
186
|
advisory: true,
|
|
179
187
|
blockerGrade: false,
|
|
188
|
+
deferred: false,
|
|
180
189
|
count: edges.length,
|
|
181
190
|
templateInterpolationCount,
|
|
182
191
|
otherNonLiteralCount,
|
|
@@ -313,17 +313,25 @@ function deepModuleCoachHtml(coach) {
|
|
|
313
313
|
|
|
314
314
|
function stewardNudgeHtml(nudge) {
|
|
315
315
|
if (!nudge || nudge.notAScore !== true) return '';
|
|
316
|
+
const unfinished = Boolean(
|
|
317
|
+
nudge.emptyStewardsPastGrace || (nudge.needsStewards && (nudge.stewardCount ?? 0) === 0)
|
|
318
|
+
);
|
|
316
319
|
const ask =
|
|
317
|
-
(nudge.needsStewards || nudge.drift
|
|
320
|
+
(nudge.needsStewards || nudge.drift || nudge.emptyStewardsPastGrace) &&
|
|
321
|
+
typeof nudge.ask === 'string' &&
|
|
322
|
+
nudge.ask
|
|
318
323
|
? `<p>${esc(nudge.ask)}</p>`
|
|
319
324
|
: '<p class="muted">No steward list gap (advisory).</p>';
|
|
320
325
|
const next =
|
|
321
326
|
typeof nudge.nextAction === 'string' && nudge.nextAction
|
|
322
327
|
? `<p class="muted">Next: ${esc(nudge.nextAction)}</p>`
|
|
323
328
|
: '';
|
|
329
|
+
const qualifier = unfinished
|
|
330
|
+
? '(unfinished residual — changes finished, not check valid)'
|
|
331
|
+
: '(advisory — never changes the check valid bit)';
|
|
324
332
|
return `
|
|
325
333
|
<section class="section card" data-advisory="stewardNudge">
|
|
326
|
-
<h2>Stewards <span class="muted"
|
|
334
|
+
<h2>Stewards <span class="muted">${qualifier}</span></h2>
|
|
327
335
|
${ask}
|
|
328
336
|
${next}
|
|
329
337
|
<p class="muted">GitHub handle or email. Never invent names. Always <code>notAScore</code>.</p>
|
package/bin/lib/html-report.mjs
CHANGED
|
@@ -110,7 +110,7 @@ export function baselineSignalHint(signal) {
|
|
|
110
110
|
export function modeBadgeHint(mode) {
|
|
111
111
|
switch (String(mode || '').toLowerCase()) {
|
|
112
112
|
case 'enforce':
|
|
113
|
-
return 'Contract matches the tree
|
|
113
|
+
return 'Contract matches the tree on checked import edges. Required GitHub status (or an advisory-only ack) is what adopts the merge boundary.';
|
|
114
114
|
case 'adapt':
|
|
115
115
|
return 'Contract is live but still aligning (optional cores with files, empty cores, or presentation-bag false green).';
|
|
116
116
|
case 'suggest':
|
|
@@ -255,7 +255,7 @@ export function computeReportFitness({ coverage, violations, ok, enforcement, co
|
|
|
255
255
|
const modeBlurb = {
|
|
256
256
|
suggest: 'Starter shape — expand layers as the codebase grows.',
|
|
257
257
|
adapt: 'Contract is live; raise governed coverage or match real folders.',
|
|
258
|
-
enforce: 'Contract
|
|
258
|
+
enforce: 'Contract matches the tree on checked import edges. Merge is adopted only with a required GitHub status or an advisory-only ack.',
|
|
259
259
|
}[mode];
|
|
260
260
|
const scoreCoverage = governedPercent == null ? 50 : governedPercent;
|
|
261
261
|
const scoreClean =
|
|
@@ -635,6 +635,16 @@ export function runInstallAgentGates(args) {
|
|
|
635
635
|
homeResults.push({ relativePath: dir, status: 'failed' });
|
|
636
636
|
}
|
|
637
637
|
if (homeResults.length === 0) {
|
|
638
|
+
const skillName = (skill) =>
|
|
639
|
+
Array.isArray(skill) ? skill[0] : skill?.name || skill;
|
|
640
|
+
const projectHasCatalog = skills.some((skill) =>
|
|
641
|
+
fs.existsSync(path.join(root, '.agents', 'skills', skillName(skill), 'SKILL.md'))
|
|
642
|
+
);
|
|
643
|
+
if (projectHasCatalog) {
|
|
644
|
+
console.log(
|
|
645
|
+
' Project .agents/skills already has this catalog; home write is optional. Prefer the project copy.'
|
|
646
|
+
);
|
|
647
|
+
}
|
|
638
648
|
for (const result of installSkillCatalog({
|
|
639
649
|
directory: dir,
|
|
640
650
|
skills,
|
|
@@ -79,11 +79,14 @@ export function evaluateInvariantCoverage(input) {
|
|
|
79
79
|
if (!covered || partial) {
|
|
80
80
|
// Enforced + proven uncovered → failsStrict; partial always advisory (never fake green).
|
|
81
81
|
const failsStrict = inv.mode === 'enforced' && !partial;
|
|
82
|
+
const kind = testGlobsMissing || testFiles.length === 0 ? 'never-had-tests' : 'tests-disappeared';
|
|
82
83
|
violations.push({
|
|
83
84
|
ruleId: 'INVARIANT_UNCOVERED',
|
|
84
85
|
message: partial
|
|
85
|
-
? `Invariant ${inv.id} coverage cannot be proven (test globs missing or empty); reporting partial, not covered.`
|
|
86
|
-
:
|
|
86
|
+
? `Invariant ${inv.id} coverage cannot be proven (test globs missing or empty); reporting partial, not covered (never-had-tests).`
|
|
87
|
+
: kind === 'tests-disappeared'
|
|
88
|
+
? `Invariant ${inv.id} is not covered by a test title or declared symbol (tests-disappeared — suite exists).`
|
|
89
|
+
: `Invariant ${inv.id} is not covered by a test title or declared symbol (never-had-tests).`,
|
|
87
90
|
file: inv.provenance.sourceFile,
|
|
88
91
|
line: 1,
|
|
89
92
|
arkruleId: inv.id,
|
|
@@ -91,6 +94,7 @@ export function evaluateInvariantCoverage(input) {
|
|
|
91
94
|
fromLayer: inv.provenance.layer,
|
|
92
95
|
severity: failsStrict ? 'error' : 'warning',
|
|
93
96
|
failsStrict,
|
|
97
|
+
kind,
|
|
94
98
|
});
|
|
95
99
|
}
|
|
96
100
|
}
|
|
@@ -134,7 +134,7 @@ const HOST_SIGNALS = {
|
|
|
134
134
|
],
|
|
135
135
|
codex: ['.codex/hooks.json', '.codex/config.toml', '.agents/skills/ark-upgrade/SKILL.md'],
|
|
136
136
|
grok: ['.grok/config.toml', '.grok/hooks/ark-write-gate.json', '.grok/skills/ark-upgrade/SKILL.md'],
|
|
137
|
-
antigravity: ['.agents/hooks.json'
|
|
137
|
+
antigravity: ['.agents/hooks.json'],
|
|
138
138
|
opencode: ['opencode.json', '.opencode/skills/ark-upgrade/SKILL.md'],
|
|
139
139
|
windsurf: ['.windsurf/rules/ark.md', '.windsurf/workflows/ark-upgrade.md'],
|
|
140
140
|
cline: ['.clinerules/ark.md', '.clinerules/workflows/ark-upgrade.md'],
|
|
@@ -307,8 +307,13 @@ function resolveSelection(root, options, manifest) {
|
|
|
307
307
|
const explicit = normalizeToolsList(options.tools);
|
|
308
308
|
const compactHost = compactRouterHost(root);
|
|
309
309
|
let hosts;
|
|
310
|
-
if (explicit.length > 0)
|
|
311
|
-
|
|
310
|
+
if (explicit.length > 0) {
|
|
311
|
+
const kept = manifest
|
|
312
|
+
? normalizeHosts(manifest.hosts)
|
|
313
|
+
: detectedManagedHosts(root);
|
|
314
|
+
// Preview/apply default is hosts-keep: --tools unions, never retires other hosts.
|
|
315
|
+
hosts = normalizeHosts([...kept, ...explicit]);
|
|
316
|
+
} else if (manifest) hosts = normalizeHosts(manifest.hosts);
|
|
312
317
|
else if (compactHost !== null) hosts = compactHost === 'none' ? [] : normalizeHosts([compactHost]);
|
|
313
318
|
else {
|
|
314
319
|
hosts = detectedManagedHosts(root);
|
package/bin/lib/mcp-adoption.mjs
CHANGED
|
@@ -17,6 +17,11 @@ import { detectActiveAgentHost, skillTemplateNames } from './skill-install.mjs';
|
|
|
17
17
|
import { detectDeployPathQuality } from './deploy-path.mjs';
|
|
18
18
|
import { collectWeakestLinkGaps } from './weakest-link.mjs';
|
|
19
19
|
import { codexRuntimeActivation, withCiProviderEvidence } from './enforcement-state.mjs';
|
|
20
|
+
import {
|
|
21
|
+
classifyAdopted,
|
|
22
|
+
readAdoptionStance,
|
|
23
|
+
NOT_ADOPTED_NEXT_ACTION,
|
|
24
|
+
} from './adoption-stance.mjs';
|
|
20
25
|
|
|
21
26
|
export { detectDeployPathQuality };
|
|
22
27
|
|
|
@@ -528,6 +533,20 @@ export function collectAdoptionGaps(root, config, coverage) {
|
|
|
528
533
|
gaps.push(g);
|
|
529
534
|
}
|
|
530
535
|
|
|
536
|
+
const adoptedKind = classifyAdopted({
|
|
537
|
+
stance: readAdoptionStance(root),
|
|
538
|
+
github: weakest.github,
|
|
539
|
+
});
|
|
540
|
+
if (adoptedKind === 'not-adopted') {
|
|
541
|
+
gaps.push({
|
|
542
|
+
id: 'adoption-stance-missing',
|
|
543
|
+
severity: 'warn',
|
|
544
|
+
message:
|
|
545
|
+
'Merge boundary not adopted: require a GitHub status on arkgate-check --strict-merge, or write .ark/adoption-stance.json with stance: "advisory-only".',
|
|
546
|
+
fix: NOT_ADOPTED_NEXT_ACTION,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
531
550
|
return {
|
|
532
551
|
gaps,
|
|
533
552
|
hosts,
|
|
@@ -42,7 +42,7 @@ function repositoryRoot(root) {
|
|
|
42
42
|
return result.status === 0 ? result.stdout.trim() : null;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function discoverLocalBaseRef(root) {
|
|
45
|
+
export function discoverLocalBaseRef(root) {
|
|
46
46
|
const top = repositoryRoot(root);
|
|
47
47
|
if (!top) return null;
|
|
48
48
|
const remoteHead = runGit(top, ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']);
|
|
@@ -92,10 +92,14 @@ export function mergePostGreenTopActions(actions, postGreen) {
|
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
94
|
* Whether doctor may print “Healthy — nothing to do”.
|
|
95
|
+
* Empty actions + !designWeak is not Healthy unless the merge boundary is
|
|
96
|
+
* required-merge (advisory-only ack is adopted but not this Healthy string).
|
|
95
97
|
* @param {{ designWeak?: boolean } | null | undefined} designFitness
|
|
96
98
|
* @param {string[]} topActions
|
|
99
|
+
* @param {string | null | undefined} adopted
|
|
97
100
|
*/
|
|
98
|
-
export function isDoctorHealthyNothingToDo(designFitness, topActions = []) {
|
|
101
|
+
export function isDoctorHealthyNothingToDo(designFitness, topActions = [], adopted = null) {
|
|
99
102
|
if (designFitness?.designWeak) return false;
|
|
103
|
+
if (adopted !== 'required-merge') return false;
|
|
100
104
|
return !topActions.some(Boolean);
|
|
101
105
|
}
|
package/bin/lib/presets.mjs
CHANGED
|
@@ -179,6 +179,28 @@ export const PERSISTENCE_PATH_PATTERNS = Object.freeze([
|
|
|
179
179
|
* Application orchestration under lib without the whole-src-lib vacuum.
|
|
180
180
|
* Never use lone `src/**` or bare `src/lib/**` as Application on Next/event trees.
|
|
181
181
|
*/
|
|
182
|
+
/** Types and constants — SharedKernel, not Application vacuum. */
|
|
183
|
+
export const SHARED_KERNEL_PATH_PATTERNS = Object.freeze([
|
|
184
|
+
'src/shared/**',
|
|
185
|
+
'src/types/**',
|
|
186
|
+
'src/**/types.ts',
|
|
187
|
+
'src/**/constants.ts',
|
|
188
|
+
'src/**/types/**',
|
|
189
|
+
'src/**/constants/**',
|
|
190
|
+
'**/shared/kernel/**',
|
|
191
|
+
'src/shared/kernel/**',
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
/** Wiring / DI / bootstrap — CompositionRoot, not Domain. */
|
|
195
|
+
export const COMPOSITION_ROOT_PATH_PATTERNS = Object.freeze([
|
|
196
|
+
'src/**/composition/**',
|
|
197
|
+
'src/**/factories/**',
|
|
198
|
+
'src/**/container.ts',
|
|
199
|
+
'src/**/bootstrap.ts',
|
|
200
|
+
'**/composition/**',
|
|
201
|
+
'**/factories/**',
|
|
202
|
+
]);
|
|
203
|
+
|
|
182
204
|
export const APPLICATION_LIB_ORCHESTRATION_PATTERNS = Object.freeze([
|
|
183
205
|
'src/lib/actions/**',
|
|
184
206
|
'src/lib/services/**',
|
package/bin/lib/product-copy.mjs
CHANGED
|
@@ -13,17 +13,20 @@ export const LEFTOVER_DESIGN_LABEL = 'leftover design work';
|
|
|
13
13
|
* Operating-mode title for humans (and doctor JSON `designFitness.label` prefix).
|
|
14
14
|
* @param {string|null|undefined} mode suggest|adapt|enforce
|
|
15
15
|
* @param {boolean} leftoverDesign
|
|
16
|
+
* @param {boolean} [stewardsUnset]
|
|
16
17
|
*/
|
|
17
|
-
export function operatingModeTitle(mode, leftoverDesign) {
|
|
18
|
+
export function operatingModeTitle(mode, leftoverDesign, stewardsUnset) {
|
|
18
19
|
const light = String(mode || 'enforce').toUpperCase();
|
|
19
|
-
|
|
20
|
+
if (leftoverDesign) return `${light} · ${LEFTOVER_DESIGN_LABEL}`;
|
|
21
|
+
if (stewardsUnset) return `${light} · stewards unset`;
|
|
22
|
+
return light;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
/** Short HTML/doctor badge text. */
|
|
23
26
|
export const LEFTOVER_DESIGN_BADGE = LEFTOVER_DESIGN_LABEL;
|
|
24
27
|
|
|
25
28
|
export const POST_GREEN_HUMAN =
|
|
26
|
-
'Imports check out, but the design is still messy.
|
|
29
|
+
'Imports check out, but the design is still messy. Next: /ark-explore, then one small refactor with /ark-autopilot and your OK.';
|
|
27
30
|
|
|
28
31
|
export const POST_GREEN_LEDE =
|
|
29
32
|
'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.';
|
package/bin/lib/remediation.mjs
CHANGED
|
@@ -41,17 +41,57 @@ export const KNOWN_FIX_CLASSES = [
|
|
|
41
41
|
'break-cycle',
|
|
42
42
|
'review-contract',
|
|
43
43
|
];
|
|
44
|
+
const PURE_SHARED_RE = /(^|\/)(constants|types|enums|shared-types|shared\/(?:types|constants)|test-projects)(\/|\.|$)|(?:^|\/)[^/]*(?:constants|types)(?:\.[cm]?[jt]sx?)?$/i;
|
|
45
|
+
const KERNEL_EMIT_RE = /(^|\/)(?:kernel(?:\/|$)|events?(?:\/|\.|$)|bootstrap(?:\.[cm]?[jt]sx?)?$|emitter(?:\.[cm]?[jt]sx?)?$)|(?:^|\/)(?:intents?|publish)(?:\/|\.|$)/i;
|
|
46
|
+
const USE_CASE_RE = /(use-?cases?|usecases?|application|orchestrat|services?|handlers?)(\/|\.|$)/i;
|
|
47
|
+
export function classifyLayerImportKind(target, extra) {
|
|
48
|
+
const value = String(target ?? '')
|
|
49
|
+
.replace(/\\/g, '/')
|
|
50
|
+
.trim();
|
|
51
|
+
const from = String(extra?.fromLayer ?? '');
|
|
52
|
+
const to = String(extra?.toLayer ?? '');
|
|
53
|
+
if (PURE_SHARED_RE.test(value))
|
|
54
|
+
return 'pure-shared';
|
|
55
|
+
if (from === 'PersistenceAdapters' &&
|
|
56
|
+
(KERNEL_EMIT_RE.test(value) || /events?|intents?|kernel|bootstrap/i.test(`${to} ${value}`))) {
|
|
57
|
+
return 'kernel-emit';
|
|
58
|
+
}
|
|
59
|
+
if (USE_CASE_RE.test(value) ||
|
|
60
|
+
((from === 'DomainModel' || from === 'ApplicationOrchestration') &&
|
|
61
|
+
to === 'PersistenceAdapters')) {
|
|
62
|
+
return 'use-case';
|
|
63
|
+
}
|
|
64
|
+
if (!value)
|
|
65
|
+
return 'unknown';
|
|
66
|
+
return 'unknown';
|
|
67
|
+
}
|
|
68
|
+
export function layerImportNextAction(violation) {
|
|
69
|
+
if (violation.typeOnly || violation.targetTypeOnlyExports || violation.namedBindingsTypeOnly) {
|
|
70
|
+
return 'Move the referenced type to a mutually allowed layer, use `import type`, then preflight again.';
|
|
71
|
+
}
|
|
72
|
+
if (violation.peerIsolation) {
|
|
73
|
+
return 'Extract the shared dependency to a shared layer, test at the public interface, then preflight again.';
|
|
74
|
+
}
|
|
75
|
+
const kind = classifyLayerImportKind(typeof violation.target === 'string' ? violation.target : '', {
|
|
76
|
+
fromLayer: typeof violation.fromLayer === 'string' ? violation.fromLayer : undefined,
|
|
77
|
+
toLayer: typeof violation.toLayer === 'string' ? violation.toLayer : undefined,
|
|
78
|
+
});
|
|
79
|
+
if (kind === 'pure-shared') {
|
|
80
|
+
return 'Adopt the imported constants/types/pure module into DomainModel or SharedKernel (do not inject a port). Then preflight again.';
|
|
81
|
+
}
|
|
82
|
+
if (kind === 'kernel-emit') {
|
|
83
|
+
return 'Persistence must not emit. Inject a port or move the event map to SharedTypes; do not import kernel/events/bootstrap from a repository. Then preflight again.';
|
|
84
|
+
}
|
|
85
|
+
if (kind === 'use-case' || violation.portProofEligible) {
|
|
86
|
+
return `Define a port in ${violation.fromLayer ?? 'the source layer'}, inject the ${violation.toLayer ?? 'outer-layer'} implementation, test at the public interface, then preflight again.`;
|
|
87
|
+
}
|
|
88
|
+
return 'Classify the import: if it is constants/types/pure, adopt into DomainModel or SharedKernel; define a port only if the target is a real use-case. Then preflight again.';
|
|
89
|
+
}
|
|
44
90
|
/** One deterministic re-entry action shared by human and machine denial surfaces. */
|
|
45
91
|
export function deterministicNextAction(violation) {
|
|
46
92
|
switch (violation.ruleId) {
|
|
47
93
|
case 'LAYER_IMPORT_VIOLATION':
|
|
48
|
-
|
|
49
|
-
return 'Move the referenced type to a mutually allowed layer, use `import type`, then preflight again.';
|
|
50
|
-
}
|
|
51
|
-
if (violation.peerIsolation) {
|
|
52
|
-
return 'Extract the shared dependency to a shared layer, test at the public interface, then preflight again.';
|
|
53
|
-
}
|
|
54
|
-
return `Define a port in ${violation.fromLayer ?? 'the source layer'}, inject the ${violation.toLayer ?? 'outer-layer'} implementation, test at the public interface, then preflight again.`;
|
|
94
|
+
return layerImportNextAction(violation);
|
|
55
95
|
case 'FORBIDDEN_GLOBAL':
|
|
56
96
|
return `Inject ${violation.target ?? 'the capability'} through a port, test at the public interface, then preflight again.`;
|
|
57
97
|
case 'CAPABILITY_VIOLATION':
|
|
@@ -222,9 +262,33 @@ export function enrichViolationWithFixClass(violation) {
|
|
|
222
262
|
'Cross-slice import blocked (peerIsolation). Do not import another feature/context directly — extract shared code to a shared layer, or coordinate via events/ports. Moving code across slices is a judgment call, not a mechanical auto-fix.';
|
|
223
263
|
}
|
|
224
264
|
else {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
265
|
+
const kind = classifyLayerImportKind(typeof violation.target === 'string' ? violation.target : '', {
|
|
266
|
+
fromLayer: typeof violation.fromLayer === 'string' ? violation.fromLayer : undefined,
|
|
267
|
+
toLayer: typeof violation.toLayer === 'string' ? violation.toLayer : undefined,
|
|
268
|
+
});
|
|
269
|
+
if (kind === 'pure-shared') {
|
|
270
|
+
enriched.fixClass = 'file-move';
|
|
271
|
+
enriched.effort = 'small';
|
|
272
|
+
enriched.enthusiastHint =
|
|
273
|
+
'This import is constants/types/pure — adopt it into DomainModel or SharedKernel. Do not inject a port.';
|
|
274
|
+
}
|
|
275
|
+
else if (kind === 'kernel-emit') {
|
|
276
|
+
enriched.fixClass = 'port-inversion';
|
|
277
|
+
enriched.effort = 'medium';
|
|
278
|
+
enriched.enthusiastHint =
|
|
279
|
+
'A repository must not import kernel/events/bootstrap. Persistence does not emit — inject a port or move the map to SharedTypes.';
|
|
280
|
+
}
|
|
281
|
+
else if (kind === 'use-case' || violation.portProofEligible) {
|
|
282
|
+
enriched.fixClass = 'port-inversion';
|
|
283
|
+
enriched.effort = 'medium';
|
|
284
|
+
enriched.enthusiastHint = `${violation.fromLayer ?? 'This layer'} must not import ${violation.toLayer ?? 'that layer'} directly. Define an interface (port) where you need the capability and inject the implementation from the outer layer.`;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
enriched.fixClass = 'review-contract';
|
|
288
|
+
enriched.effort = 'medium';
|
|
289
|
+
enriched.enthusiastHint =
|
|
290
|
+
'Do not assume a port. If the target is constants/types/pure, adopt it into Domain or SharedKernel; define a port only for a real use-case.';
|
|
291
|
+
}
|
|
228
292
|
}
|
|
229
293
|
break;
|
|
230
294
|
case 'FORBIDDEN_GLOBAL':
|
|
@@ -688,7 +688,9 @@ export function assessCodexSkillParity(root) {
|
|
|
688
688
|
|
|
689
689
|
const repoNeedsAttention =
|
|
690
690
|
repoInPlay && (repo.missing > 0 || repo.stale > 0 || repo.legacyPromptsOnly);
|
|
691
|
+
const repoCatalogComplete = repoInPlay && repo.missing === 0 && !repo.legacyPromptsOnly;
|
|
691
692
|
const homeNeedsAttention =
|
|
693
|
+
!repoCatalogComplete &&
|
|
692
694
|
newerHomeCatalog === null &&
|
|
693
695
|
homeInPlay &&
|
|
694
696
|
(home.missing > 0 ||
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
/** Fail-closed completeness evidence for one proposed source snippet. */
|
|
2
2
|
import { ANALYSIS_COMPLETENESS } from './analysis-completeness.mjs';
|
|
3
3
|
|
|
4
|
+
export function flattenTsParseDiagnostics(ts, diagnostics, sourceFile) {
|
|
5
|
+
if (!Array.isArray(diagnostics) || !ts) return [];
|
|
6
|
+
return diagnostics.map((diagnostic) => {
|
|
7
|
+
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
|
|
8
|
+
let line = 1;
|
|
9
|
+
let column = 1;
|
|
10
|
+
if (typeof diagnostic.start === 'number' && sourceFile?.getLineAndCharacterOfPosition) {
|
|
11
|
+
const pos = sourceFile.getLineAndCharacterOfPosition(diagnostic.start);
|
|
12
|
+
line = (pos.line ?? 0) + 1;
|
|
13
|
+
column = (pos.character ?? 0) + 1;
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
line,
|
|
17
|
+
column,
|
|
18
|
+
message: String(message || '').trim() || 'parse error',
|
|
19
|
+
code: diagnostic.code,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
4
24
|
function finding(ruleId, message, file, nextAction) {
|
|
5
25
|
return {
|
|
6
26
|
ruleId,
|
|
@@ -54,6 +74,11 @@ export function validateSnippetAnalysis({ gate, ts, source, context = {} }) {
|
|
|
54
74
|
if (!Array.isArray(parsed.parseDiagnostics)) throw new Error('parse diagnostics unavailable');
|
|
55
75
|
const diagnosticCount = parsed.parseDiagnostics.length;
|
|
56
76
|
if (diagnosticCount > 0) {
|
|
77
|
+
const tsDiagnostics = flattenTsParseDiagnostics(ts, parsed.parseDiagnostics, parsed);
|
|
78
|
+
const first = tsDiagnostics[0];
|
|
79
|
+
const detail = first
|
|
80
|
+
? `line ${first.line}: ${first.message}`
|
|
81
|
+
: `${diagnosticCount} parse diagnostic(s)`;
|
|
57
82
|
return {
|
|
58
83
|
mode: 'lexical-compatibility',
|
|
59
84
|
valid: false,
|
|
@@ -62,18 +87,25 @@ export function validateSnippetAnalysis({ gate, ts, source, context = {} }) {
|
|
|
62
87
|
completenessReasons: [
|
|
63
88
|
{
|
|
64
89
|
code: 'ANALYSIS_PARSE_INCOMPLETE',
|
|
65
|
-
message: `The proposed source has ${diagnosticCount} parse diagnostic(s)
|
|
90
|
+
message: `The proposed source has ${diagnosticCount} parse diagnostic(s). ${detail}`,
|
|
66
91
|
...(file ? { file } : {}),
|
|
92
|
+
line: first?.line,
|
|
93
|
+
tsDiagnostics,
|
|
67
94
|
},
|
|
68
95
|
],
|
|
69
96
|
violations: [
|
|
70
97
|
...base.violations,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
98
|
+
{
|
|
99
|
+
...finding(
|
|
100
|
+
'ANALYSIS_PARSE_INCOMPLETE',
|
|
101
|
+
`Analysis partial: ${detail}`,
|
|
102
|
+
file,
|
|
103
|
+
'Incremental mid-edit parse errors are normal. Finish the source, then re-run `npx arkgate-check` (or the write hook). Do not call ark_prepare_change from a hook deny.'
|
|
104
|
+
),
|
|
105
|
+
line: first?.line ?? 1,
|
|
106
|
+
column: first?.column ?? 1,
|
|
107
|
+
evidence: { tsDiagnostics, diagnosticCount },
|
|
108
|
+
},
|
|
77
109
|
],
|
|
78
110
|
};
|
|
79
111
|
}
|
|
@@ -87,7 +119,7 @@ export function validateSnippetAnalysis({ gate, ts, source, context = {} }) {
|
|
|
87
119
|
{
|
|
88
120
|
code: 'LEXICAL_EVIDENCE_INCOMPLETE',
|
|
89
121
|
message:
|
|
90
|
-
'Single-file validation cannot prove project module resolution or
|
|
122
|
+
'Single-file validation cannot prove project module resolution. The write hook is already the verdict, or re-run `npx arkgate-check --root . --config ark.config.json`. Do not call ark_prepare_change from a hook deny.',
|
|
91
123
|
...(file ? { file } : {}),
|
|
92
124
|
},
|
|
93
125
|
],
|
|
@@ -151,7 +151,7 @@ export function renderStartPreview(preview, options = {}) {
|
|
|
151
151
|
console.log('Apply this plan with: arkgate start --apply');
|
|
152
152
|
}
|
|
153
153
|
if (preview.analysis) {
|
|
154
|
-
console.log(`Your project looks like: ${preview.analysis.label}
|
|
154
|
+
console.log(`Your project looks like: ${preview.analysis.label}.`);
|
|
155
155
|
}
|
|
156
156
|
console.log(applying ? 'Files create/edit/delete:' : 'Files to create/edit/delete:');
|
|
157
157
|
if (preview.changes.length === 0) console.log(' (none)');
|
|
@@ -159,34 +159,24 @@ export function renderStartPreview(preview, options = {}) {
|
|
|
159
159
|
console.log(` ${change.action.padEnd(6)} ${change.path}`);
|
|
160
160
|
}
|
|
161
161
|
if (!applying) {
|
|
162
|
-
console.log('
|
|
163
|
-
|
|
162
|
+
console.log('Setup: install package + host gates (see --json).');
|
|
163
|
+
console.log('Preview does not write. Apply installs CI.');
|
|
164
164
|
}
|
|
165
|
-
console.log('Host guarantees:');
|
|
166
|
-
for (const guarantee of preview.hostGuarantees) console.log(` ${guarantee}`);
|
|
167
165
|
if (preview.runtimeActivation) {
|
|
168
|
-
console.log('Codex
|
|
169
|
-
console.log(` Runtime activation: ${JSON.stringify(preview.runtimeActivation)}`);
|
|
170
|
-
console.log(` Restart Codex, then call ark_identity with expectedRoot "${preview.root}".`);
|
|
171
|
-
console.log(' Do not trust MCP verdicts before the project identity matches.');
|
|
166
|
+
console.log('Host: Codex is configured but not verified yet. Restart the host, then confirm this project.');
|
|
172
167
|
}
|
|
173
168
|
if (preview.unresolvedDecisions.length > 0) {
|
|
174
169
|
console.log('Unresolved decisions:');
|
|
175
170
|
for (const decision of preview.unresolvedDecisions) console.log(` ${decision}`);
|
|
176
171
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
);
|
|
186
|
-
for (const change of preview.changes) {
|
|
187
|
-
console.log(` ${change.action.padEnd(6)} ${change.path} ${change.afterHash ?? '(deleted)'}`);
|
|
188
|
-
}
|
|
189
|
-
if (!applying) {
|
|
172
|
+
if (applying) {
|
|
173
|
+
const percent = preview.projectedCoverage?.percent;
|
|
174
|
+
if (percent != null) console.log(`Projected governed coverage: ${percent}%`);
|
|
175
|
+
const verified = (preview.hostGuarantees || []).find((line) =>
|
|
176
|
+
String(line).startsWith('Hard-write hook verified')
|
|
177
|
+
);
|
|
178
|
+
if (verified) console.log(verified);
|
|
179
|
+
} else {
|
|
190
180
|
console.log('Review complete file contents with --json.');
|
|
191
181
|
}
|
|
192
182
|
}
|
|
@@ -26,6 +26,7 @@ import { readBaseline } from './violations.mjs';
|
|
|
26
26
|
import { reportsDir, readJsonSafe } from './html-report.mjs';
|
|
27
27
|
import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
|
|
28
28
|
import { collectVsBaseFacts, discoverTeamBaseRef } from './team-parliament-io.mjs';
|
|
29
|
+
import { classifyAdopted, readAdoptionStance } from './adoption-stance.mjs';
|
|
29
30
|
|
|
30
31
|
function sha256Hex(value) {
|
|
31
32
|
return createHash('sha256').update(value, 'utf8').digest('hex');
|
|
@@ -387,6 +388,21 @@ export function collectStatusFacts(options = {}) {
|
|
|
387
388
|
latest?.leftoverDesignWork === true ||
|
|
388
389
|
latest?.designFitness?.designWeak === true ||
|
|
389
390
|
latest?.doctor?.designFitness?.designWeak === true,
|
|
391
|
+
adopted:
|
|
392
|
+
options.adopted ??
|
|
393
|
+
classifyAdopted({
|
|
394
|
+
stance: readAdoptionStance(resolvedRoot),
|
|
395
|
+
github: {
|
|
396
|
+
requiredStatusConfigured: writePath?.enforcementState?.ciMerge?.required === true,
|
|
397
|
+
arkCheckRequired: writePath?.enforcementState?.ciMerge?.required === true,
|
|
398
|
+
},
|
|
399
|
+
ci: {
|
|
400
|
+
state:
|
|
401
|
+
writePath?.enforcementState?.ciMerge?.required === true
|
|
402
|
+
? 'required'
|
|
403
|
+
: undefined,
|
|
404
|
+
},
|
|
405
|
+
}),
|
|
390
406
|
improvementCompass,
|
|
391
407
|
vsBase: (() => {
|
|
392
408
|
const vsRef = typeof options.vs === 'string' ? options.vs.trim() : '';
|
|
@@ -242,9 +242,15 @@ export function resolveStatusNextAction(facts, binding, activation, lastCheck, r
|
|
|
242
242
|
summary: 'ArkRules residual remains frozen — review inventory debt without claiming a score.',
|
|
243
243
|
};
|
|
244
244
|
}
|
|
245
|
+
if (facts.adopted === 'required-merge' || facts.adopted === 'advisory-only-acked') {
|
|
246
|
+
return {
|
|
247
|
+
id: 'stay-enforced',
|
|
248
|
+
summary: 'Contract looks enforceable for this session — keep writing through the gate and re-check after structural edits.',
|
|
249
|
+
};
|
|
250
|
+
}
|
|
245
251
|
return {
|
|
246
|
-
id: '
|
|
247
|
-
summary: '
|
|
252
|
+
id: 'require-ci-merge-status',
|
|
253
|
+
summary: 'Make arkgate-check --strict-merge a required GitHub status, or write .ark/adoption-stance.json with stance: "advisory-only".',
|
|
248
254
|
};
|
|
249
255
|
}
|
|
250
256
|
export function buildStatusManifest(facts) {
|