arkgate 4.8.13 → 4.8.15
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 +219 -4
- package/README.md +33 -22
- package/SECURITY.md +5 -3
- package/bin/ark-check-runtime.mjs +25 -57
- package/bin/ark-mcp-runtime.mjs +111 -2
- package/bin/ark-shared.mjs +140 -11
- package/bin/ark.mjs +7 -25
- package/bin/lib/adr-presence.mjs +97 -0
- package/bin/lib/agent-projection-formatters.mjs +2 -0
- package/bin/lib/agent-skills-package.mjs +241 -8
- package/bin/lib/analysis-completeness.mjs +38 -2
- package/bin/lib/analysis-engine.mjs +6 -6
- package/bin/lib/architecture-scan.mjs +33 -5
- package/bin/lib/ark-order-doctor.mjs +7 -1
- package/bin/lib/ark-order-report.mjs +2 -1
- package/bin/lib/ark-run-doctor.mjs +6 -0
- package/bin/lib/ark-run-report.mjs +6 -2
- package/bin/lib/arkrules-sensors.mjs +81 -0
- package/bin/lib/baseline-key.mjs +4 -1
- package/bin/lib/check-args.mjs +52 -2
- package/bin/lib/ci-and-commands.mjs +7 -2
- package/bin/lib/config-contract.mjs +64 -1
- package/bin/lib/design-smells.mjs +21 -1
- package/bin/lib/diagnostic-catalog.mjs +5 -3
- package/bin/lib/doctor-advisories.mjs +109 -17
- package/bin/lib/doctor-human.mjs +20 -6
- package/bin/lib/doctor-next-actions.mjs +14 -0
- package/bin/lib/doctor-plan.mjs +32 -2
- package/bin/lib/enforcement-honesty.mjs +47 -2
- package/bin/lib/first-run-help.mjs +19 -3
- package/bin/lib/gate-files.mjs +40 -3
- package/bin/lib/host-support-matrix.mjs +9 -1
- package/bin/lib/html-report-depth.mjs +2 -0
- package/bin/lib/html-report.mjs +13 -2
- package/bin/lib/import-resolve.mjs +74 -13
- package/bin/lib/improvement-compass-doctor.mjs +6 -1
- package/bin/lib/improvement-compass-map.mjs +3 -1
- package/bin/lib/install-migrate.mjs +23 -0
- package/bin/lib/layer-description.mjs +90 -8
- package/bin/lib/mcp-hook-payload.mjs +57 -1
- package/bin/lib/no-domain-frontend.mjs +91 -0
- package/bin/lib/presets.mjs +3 -4
- package/bin/lib/product-copy.mjs +19 -0
- package/bin/lib/projected-governed-coverage.mjs +114 -0
- package/bin/lib/recommend-cli.mjs +54 -0
- package/bin/lib/remediation.mjs +6 -2
- package/bin/lib/resolved-candidate-facts.mjs +225 -100
- package/bin/lib/rules-under-contract.mjs +117 -2
- package/bin/lib/scan-files.mjs +39 -0
- package/bin/lib/snippet-analysis.mjs +14 -8
- package/bin/lib/start-preview.mjs +8 -2
- package/bin/lib/states-transitions-presence.mjs +212 -0
- package/bin/lib/status-command.mjs +2 -0
- package/bin/lib/status-transition-catalog.mjs +410 -0
- package/bin/lib/team-parliament-io.mjs +10 -0
- package/bin/lib/upgrade-whats-new.mjs +3 -3
- package/bin/lib/violations.mjs +43 -1
- package/bin/lib/write-path-capabilities.mjs +20 -5
- package/bin/lib/write-path-detect.mjs +27 -2
- package/dist/{configTypes-j7so8B4O.d.ts → configTypes-Dt3DpVbd.d.ts} +19 -0
- package/dist/{diagnosticCatalog-DA565Lja.d.ts → diagnosticCatalog-BEg85XlE.d.ts} +3 -3
- package/dist/eslint/index.cjs +4 -4
- package/dist/eslint/index.d.ts +1 -1
- package/dist/eslint/index.js +4 -4
- package/dist/index.cjs +31 -31
- package/dist/index.d.ts +126 -19
- package/dist/index.js +31 -31
- package/dist/nestjs/index.cjs +5 -5
- package/dist/nestjs/index.d.ts +3 -3
- package/dist/nestjs/index.js +5 -5
- package/dist/runtime/index.cjs +15 -15
- package/dist/runtime/index.d.ts +6 -6
- package/dist/runtime/index.js +15 -15
- package/dist/{types-Djbs3KjE.d.ts → types-CN9tVMPz.d.ts} +3 -1
- package/dist/{types-tGhZUiGX.d.ts → types-TBiv0WHL.d.ts} +1 -1
- package/docs/README.md +8 -6
- package/docs/agent-guide.md +35 -17
- package/docs/ai-gates.md +13 -4
- package/docs/arkorder.md +11 -4
- package/docs/brownfield-adoption.md +4 -1
- package/docs/configuration.md +63 -14
- package/docs/develop.md +4 -1
- package/docs/diagnostics.md +30 -10
- package/docs/enthusiast/README.md +6 -1
- package/docs/enthusiast/how-to-agent-gates.md +5 -0
- package/docs/enthusiast/how-to-gallery-starter.md +2 -1
- package/docs/enthusiast/how-to-pick-shape.md +1 -1
- package/docs/package-surface.md +10 -6
- package/docs/product-voice.md +32 -6
- package/docs/threat-model.md +2 -2
- package/docs/typescript-support.md +3 -3
- package/docs/use.md +23 -11
- package/package.json +1 -1
- package/schemas/ark.config.schema.json +23 -2
- package/server.json +2 -2
- package/templates/agent-skills/README.md +7 -4
- package/templates/agent-skills/ark-adopt/SKILL.md +91 -6
- package/templates/agent-skills/ark-architect/SKILL.md +5 -18
- package/templates/agent-skills/ark-autopilot/SKILL.md +14 -6
- package/templates/agent-skills/ark-contract/SKILL.md +9 -20
- package/templates/agent-skills/ark-coverage/SKILL.md +13 -9
- package/templates/agent-skills/ark-explain/SKILL.md +11 -5
- package/templates/agent-skills/ark-explore/SKILL.md +39 -5
- package/templates/agent-skills/ark-fix/SKILL.md +15 -20
- package/templates/agent-skills/ark-loop/SKILL.md +14 -20
- package/templates/agent-skills/ark-order/SKILL.md +200 -0
- package/templates/agent-skills/ark-place/SKILL.md +49 -11
- package/templates/agent-skills/ark-runtime/SKILL.md +21 -6
- package/templates/agent-skills/ark-think/SKILL.md +24 -126
- package/templates/agent-skills/ark-upgrade/SKILL.md +14 -3
- package/templates/arkrules/DomainModel.json +14 -1
- package/templates/skills/ark-adopt.md +91 -6
- package/templates/skills/ark-architect.md +5 -18
- package/templates/skills/ark-autopilot.md +14 -6
- package/templates/skills/ark-contract.md +9 -20
- package/templates/skills/ark-coverage.md +13 -9
- package/templates/skills/ark-explain.md +11 -5
- package/templates/skills/ark-explore.md +39 -5
- package/templates/skills/ark-fix.md +15 -20
- package/templates/skills/ark-loop.md +14 -20
- package/templates/skills/ark-order.md +200 -0
- package/templates/skills/ark-place.md +49 -11
- package/templates/skills/ark-runtime.md +21 -6
- package/templates/skills/ark-think.md +24 -126
- package/templates/skills/ark-upgrade.md +14 -3
- package/templates/tests/ark-adoption-gaps.test.ts +5 -4
package/bin/ark-mcp-runtime.mjs
CHANGED
|
@@ -82,7 +82,10 @@ import {
|
|
|
82
82
|
codexPatchWrites,
|
|
83
83
|
proposedSource,
|
|
84
84
|
emitHostAllow,
|
|
85
|
+
emitHostDeny,
|
|
85
86
|
formatWriteGateDeny,
|
|
87
|
+
unclassifiedIncludedWriteDeny,
|
|
88
|
+
requiredOwnerWriteDeny,
|
|
86
89
|
} from './lib/mcp-hook-payload.mjs';
|
|
87
90
|
import {
|
|
88
91
|
canonicalizeCandidateChanges,
|
|
@@ -257,6 +260,13 @@ function inferLayer(filePath, config, root) {
|
|
|
257
260
|
return layerForFile(root, filePath, config.layers);
|
|
258
261
|
}
|
|
259
262
|
|
|
263
|
+
/** Pattern match only — layer.exclude is an intentional ungoverned hole, not a miss. */
|
|
264
|
+
function layerPatternClaimsFile(filePath, config, root) {
|
|
265
|
+
if (!filePath) return false;
|
|
266
|
+
const layers = (config.layers ?? []).map((layer) => ({ ...layer, exclude: [] }));
|
|
267
|
+
return Boolean(layerForFile(root, filePath, layers));
|
|
268
|
+
}
|
|
269
|
+
|
|
260
270
|
async function loadArk() {
|
|
261
271
|
const url = new URL('../dist/index.js', import.meta.url);
|
|
262
272
|
if (!fs.existsSync(url)) {
|
|
@@ -472,6 +482,55 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
472
482
|
const governedWrites = canonicalSourceWrites.filter((change) =>
|
|
473
483
|
isCandidateSourceInScope(config, change.path)
|
|
474
484
|
);
|
|
485
|
+
const layerlessWrites = governedWrites.filter(
|
|
486
|
+
(change) =>
|
|
487
|
+
change.delete !== true &&
|
|
488
|
+
!inferLayer(change.path, config, args.root) &&
|
|
489
|
+
!layerPatternClaimsFile(change.path, config, args.root)
|
|
490
|
+
);
|
|
491
|
+
if (layerlessWrites.length > 0) {
|
|
492
|
+
const first = layerlessWrites[0];
|
|
493
|
+
const deny = unclassifiedIncludedWriteDeny(first.path);
|
|
494
|
+
const message = formatWriteGateDeny({
|
|
495
|
+
file: first.path,
|
|
496
|
+
reason: deny.message,
|
|
497
|
+
ruleId: deny.ruleId,
|
|
498
|
+
nextAction: deny.nextAction,
|
|
499
|
+
});
|
|
500
|
+
emitHostDeny(output, {
|
|
501
|
+
antigravityStyle,
|
|
502
|
+
cursorStyle,
|
|
503
|
+
grokStyle,
|
|
504
|
+
message,
|
|
505
|
+
file: first.path,
|
|
506
|
+
});
|
|
507
|
+
output.status(2);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
const unownedWrite = governedWrites.find((change) => {
|
|
511
|
+
if (change.delete === true) return false;
|
|
512
|
+
const house = inferLayer(change.path, config, args.root);
|
|
513
|
+
return Boolean(requiredOwnerWriteDeny(config, house, change.path));
|
|
514
|
+
});
|
|
515
|
+
if (unownedWrite) {
|
|
516
|
+
const house = inferLayer(unownedWrite.path, config, args.root);
|
|
517
|
+
const deny = requiredOwnerWriteDeny(config, house, unownedWrite.path);
|
|
518
|
+
const message = formatWriteGateDeny({
|
|
519
|
+
file: unownedWrite.path,
|
|
520
|
+
reason: deny.message,
|
|
521
|
+
ruleId: deny.ruleId,
|
|
522
|
+
nextAction: deny.nextAction,
|
|
523
|
+
});
|
|
524
|
+
emitHostDeny(output, {
|
|
525
|
+
antigravityStyle,
|
|
526
|
+
cursorStyle,
|
|
527
|
+
grokStyle,
|
|
528
|
+
message,
|
|
529
|
+
file: unownedWrite.path,
|
|
530
|
+
});
|
|
531
|
+
output.status(2);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
475
534
|
const changes = governedWrites.map(({ path: relativePath, content, delete: deleted }) =>
|
|
476
535
|
deleted ? { path: relativePath, delete: true } : { path: relativePath, content }
|
|
477
536
|
);
|
|
@@ -608,6 +667,46 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
608
667
|
}
|
|
609
668
|
|
|
610
669
|
const layer = inferLayer(filePath, config, args.root);
|
|
670
|
+
if (
|
|
671
|
+
!layer &&
|
|
672
|
+
!layerPatternClaimsFile(filePath, config, args.root) &&
|
|
673
|
+
isCandidateSourceInScope(config, normalizedRel)
|
|
674
|
+
) {
|
|
675
|
+
const deny = unclassifiedIncludedWriteDeny(normalizedRel);
|
|
676
|
+
const message = formatWriteGateDeny({
|
|
677
|
+
file: normalizedRel,
|
|
678
|
+
reason: deny.message,
|
|
679
|
+
ruleId: deny.ruleId,
|
|
680
|
+
nextAction: deny.nextAction,
|
|
681
|
+
});
|
|
682
|
+
emitHostDeny(output, {
|
|
683
|
+
antigravityStyle,
|
|
684
|
+
cursorStyle,
|
|
685
|
+
grokStyle,
|
|
686
|
+
message,
|
|
687
|
+
file: normalizedRel,
|
|
688
|
+
});
|
|
689
|
+
output.status(2);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
const ownerDeny = requiredOwnerWriteDeny(config, layer, normalizedRel);
|
|
693
|
+
if (ownerDeny) {
|
|
694
|
+
const message = formatWriteGateDeny({
|
|
695
|
+
file: normalizedRel,
|
|
696
|
+
reason: ownerDeny.message,
|
|
697
|
+
ruleId: ownerDeny.ruleId,
|
|
698
|
+
nextAction: ownerDeny.nextAction,
|
|
699
|
+
});
|
|
700
|
+
emitHostDeny(output, {
|
|
701
|
+
antigravityStyle,
|
|
702
|
+
cursorStyle,
|
|
703
|
+
grokStyle,
|
|
704
|
+
message,
|
|
705
|
+
file: normalizedRel,
|
|
706
|
+
});
|
|
707
|
+
output.status(2);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
611
710
|
const validateOnce = (src) =>
|
|
612
711
|
validateSnippetAnalysis({
|
|
613
712
|
gate,
|
|
@@ -713,6 +812,10 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
713
812
|
ruleId: firstDiagnostic?.ruleId,
|
|
714
813
|
nextAction: firstDiagnostic?.nextAction,
|
|
715
814
|
extraLines: [
|
|
815
|
+
...((result.completenessReasons ?? [])
|
|
816
|
+
.filter((reason) => reason.code === 'LEXICAL_EVIDENCE_INCOMPLETE')
|
|
817
|
+
.map((reason) => reason.message)
|
|
818
|
+
.filter(Boolean)),
|
|
716
819
|
...adapterResult.diagnostics.slice(1).map(
|
|
717
820
|
(diagnostic) =>
|
|
718
821
|
`[${diagnostic.ruleId}] ${diagnostic.message}${diagnostic.location.line ? ` (line ${diagnostic.location.line})` : ''}`
|
|
@@ -952,7 +1055,9 @@ function createResidentDoctorSession(args, config, ts) {
|
|
|
952
1055
|
config,
|
|
953
1056
|
manifest: args.projectManifest,
|
|
954
1057
|
rules,
|
|
955
|
-
|
|
1058
|
+
// Resident doctor is a full-tree snapshot. Do not pass the governed list
|
|
1059
|
+
// as a --changed envelope (#212).
|
|
1060
|
+
files: [],
|
|
956
1061
|
ts,
|
|
957
1062
|
args,
|
|
958
1063
|
});
|
|
@@ -1927,6 +2032,8 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1927
2032
|
'Returns layer, mayImport / mustNotImport, forbiddenGlobals, and goldenPattern ' +
|
|
1928
2033
|
'(load-bearing for NEW code when .ark/golden-pattern.json exists — adopt generates it). ' +
|
|
1929
2034
|
'When the matched layer has layers[].description, the JSON includes description; the field is omitted when absent. ' +
|
|
2035
|
+
'When the matched layer has layers[].trustBoundary (public|auth|admin|internal), the JSON includes trustBoundary; omitted when absent. ' +
|
|
2036
|
+
'When the matched layer has layers[].owners, the JSON includes owners; omitted when absent. ' +
|
|
1930
2037
|
'Call BEFORE writing a new file. ' +
|
|
1931
2038
|
'Prefer ark_prepare_write when you already have the source snippet (place+validate+autoPatch in one call).',
|
|
1932
2039
|
inputSchema: {
|
|
@@ -1953,7 +2060,9 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1953
2060
|
'Also returns the versioned new/worsened designDelta for the proposed full file. ' +
|
|
1954
2061
|
'Composes ark_place + write-gate — call BEFORE Write/Edit when you have the snippet. ' +
|
|
1955
2062
|
'When the matched layer has layers[].description, the JSON includes description; the field is omitted when absent. ' +
|
|
1956
|
-
'
|
|
2063
|
+
'When the matched layer has layers[].trustBoundary (public|auth|admin|internal), the JSON includes trustBoundary; omitted when absent. ' +
|
|
2064
|
+
'When the matched layer has layers[].owners, the JSON includes owners; omitted when absent. ' +
|
|
2065
|
+
'Returns { filePath, layer, description?, trustBoundary?, owners?, valid, violations?, autoPatch?, judgmentBrief?, contentHash, ... }.',
|
|
1957
2066
|
inputSchema: {
|
|
1958
2067
|
type: 'object',
|
|
1959
2068
|
properties: {
|
package/bin/ark-shared.mjs
CHANGED
|
@@ -104,10 +104,85 @@ function mergeLayerPatterns(config, layerName, patterns, extras = {}) {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/** Next-flavored start captions — applied only when Next is actually detected. */
|
|
108
|
+
export const NEXT_LAYER_DESCRIPTIONS = Object.freeze({
|
|
109
|
+
ApplicationOrchestration:
|
|
110
|
+
'Use cases and services that coordinate the domain through ports. Next App Router API (`app/api/**`) and Pages API (`pages/api/**`) are orchestration shells, not UI.',
|
|
111
|
+
PresentationAdapters:
|
|
112
|
+
'Entrypoints — UI, framework app/pages dirs, controllers. Next `app/api` is Application, not this layer. Never bare lib/** (data clients are Persistence).',
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/** Library / package-monorepo captions — no framework storefront. */
|
|
116
|
+
export const LIBRARY_LAYER_DESCRIPTIONS = Object.freeze({
|
|
117
|
+
ApplicationOrchestration:
|
|
118
|
+
'Use cases and the package public surface that coordinate the domain. No I/O of its own.',
|
|
119
|
+
PresentationAdapters:
|
|
120
|
+
'Optional CLIs, docs, or demo UIs — not the published library core.',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
function setLayerDescription(config, layerName, description) {
|
|
124
|
+
const layer = config.layers?.find((entry) => entry.name === layerName);
|
|
125
|
+
if (!layer || typeof description !== 'string' || description.length === 0) return;
|
|
126
|
+
layer.description = description;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** packages/* (or similar) of published libraries — no app/cli units, no web framework. */
|
|
130
|
+
export function isLibraryPackageMonorepo(signals) {
|
|
131
|
+
if (!signals || signals.nextFramework || signals.nestFramework || signals.expressLike) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
const units = Array.isArray(signals.repoUnits) ? signals.repoUnits : [];
|
|
135
|
+
const productive = units.filter(
|
|
136
|
+
(unit) => unit.root !== '.' && !['docs', 'example', 'test'].includes(unit.role)
|
|
137
|
+
);
|
|
138
|
+
if (productive.length === 0) return false;
|
|
139
|
+
return (
|
|
140
|
+
productive.some((unit) => unit.role === 'library') &&
|
|
141
|
+
!productive.some((unit) => unit.role === 'application' || unit.role === 'cli')
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function applyFrameworkLayerCaptions(config, signals) {
|
|
146
|
+
const nextish = Boolean(
|
|
147
|
+
signals.nextFramework || (signals.ui && signals.toolHints?.includes('next'))
|
|
148
|
+
);
|
|
149
|
+
if (nextish) {
|
|
150
|
+
setLayerDescription(
|
|
151
|
+
config,
|
|
152
|
+
'ApplicationOrchestration',
|
|
153
|
+
NEXT_LAYER_DESCRIPTIONS.ApplicationOrchestration
|
|
154
|
+
);
|
|
155
|
+
setLayerDescription(
|
|
156
|
+
config,
|
|
157
|
+
'PresentationAdapters',
|
|
158
|
+
NEXT_LAYER_DESCRIPTIONS.PresentationAdapters
|
|
159
|
+
);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (
|
|
163
|
+
(signals.libraryOnly && !signals.nestFramework && !signals.expressLike) ||
|
|
164
|
+
isLibraryPackageMonorepo(signals)
|
|
165
|
+
) {
|
|
166
|
+
setLayerDescription(
|
|
167
|
+
config,
|
|
168
|
+
'ApplicationOrchestration',
|
|
169
|
+
LIBRARY_LAYER_DESCRIPTIONS.ApplicationOrchestration
|
|
170
|
+
);
|
|
171
|
+
setLayerDescription(
|
|
172
|
+
config,
|
|
173
|
+
'PresentationAdapters',
|
|
174
|
+
LIBRARY_LAYER_DESCRIPTIONS.PresentationAdapters
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
107
179
|
/**
|
|
108
180
|
* Framework-aware layout overlays. Detection uses collectRepoShapeSignals (deps + filenames).
|
|
109
|
-
* Pure additive: never removes existing preset
|
|
110
|
-
* reach meaningful governed% under hexagonal/layered without a hand-written adopt pass.
|
|
181
|
+
* Pure additive for patterns: never removes existing preset globs. Goal: Nest/Next/express
|
|
182
|
+
* starters reach meaningful governed% under hexagonal/layered without a hand-written adopt pass.
|
|
183
|
+
* Layer captions are the one replace: Next-flavored copy only when Next is present;
|
|
184
|
+
* library-native copy on library-only / library-package monorepos. Generic preset
|
|
185
|
+
* captions stay when neither applies.
|
|
111
186
|
*/
|
|
112
187
|
export function applyFrameworkLayoutOverlays(config, root) {
|
|
113
188
|
if (!config || !root) return config;
|
|
@@ -427,6 +502,7 @@ export function applyFrameworkLayoutOverlays(config, root) {
|
|
|
427
502
|
: 'library';
|
|
428
503
|
}
|
|
429
504
|
|
|
505
|
+
applyFrameworkLayerCaptions(next, signals);
|
|
430
506
|
return withArkConfigMetadata(next);
|
|
431
507
|
}
|
|
432
508
|
|
|
@@ -1082,7 +1158,9 @@ export function collectRepoShapeSignals(root) {
|
|
|
1082
1158
|
return !excludedUnitRoots.some((prefix) => rel.startsWith(prefix));
|
|
1083
1159
|
});
|
|
1084
1160
|
const discoveredFileSet = new Set(sourceFiles.map((file) => path.resolve(file)));
|
|
1085
|
-
|
|
1161
|
+
// Files under detected source roots — NOT layer-governed coverage. User-facing
|
|
1162
|
+
// "projected governed coverage" is computeCoverage (see projected-governed-coverage.mjs).
|
|
1163
|
+
const discoveredSourceCoverage = productionFiles.length === 0
|
|
1086
1164
|
? 0
|
|
1087
1165
|
: Math.round((productionFiles.filter((file) => discoveredFileSet.has(path.resolve(file))).length / productionFiles.length) * 100);
|
|
1088
1166
|
const tinyTree = sourceFileCount < 3;
|
|
@@ -1261,7 +1339,7 @@ export function collectRepoShapeSignals(root) {
|
|
|
1261
1339
|
libraryOnly,
|
|
1262
1340
|
tinyTree,
|
|
1263
1341
|
sourceFileCount,
|
|
1264
|
-
|
|
1342
|
+
discoveredSourceCoverage,
|
|
1265
1343
|
domain,
|
|
1266
1344
|
application,
|
|
1267
1345
|
domainHeavy,
|
|
@@ -1529,7 +1607,6 @@ export function scoreArchetypes(signals, playbook) {
|
|
|
1529
1607
|
confidence,
|
|
1530
1608
|
requiresConfirmation: true,
|
|
1531
1609
|
confirmationReasons: [
|
|
1532
|
-
`projected governed coverage is ${signals.projectedGovernedCoverage ?? 0}% (below 90%)`,
|
|
1533
1610
|
'no archetype received a positive score',
|
|
1534
1611
|
],
|
|
1535
1612
|
phases: fallback.phases,
|
|
@@ -1554,9 +1631,7 @@ export function scoreArchetypes(signals, playbook) {
|
|
|
1554
1631
|
confidence = Math.min(confidence, 0.28);
|
|
1555
1632
|
}
|
|
1556
1633
|
const closeRecommendations = Boolean(second && top.score - second.score <= 2);
|
|
1557
|
-
const lowProjectedCoverage = (signals.projectedGovernedCoverage ?? 0) < 90;
|
|
1558
1634
|
if (closeRecommendations) confidence = Math.min(confidence, 0.49);
|
|
1559
|
-
if (lowProjectedCoverage) confidence = Math.min(confidence, 0.49);
|
|
1560
1635
|
|
|
1561
1636
|
return {
|
|
1562
1637
|
ranked: scored,
|
|
@@ -1571,10 +1646,9 @@ export function scoreArchetypes(signals, playbook) {
|
|
|
1571
1646
|
'TypeScript/JS surface is thin or missing — treat the archetype as a weak hint. Prefer ark-check --suggest-include / --adopt-contract on the real package roots before scaffolding.',
|
|
1572
1647
|
}
|
|
1573
1648
|
: {}),
|
|
1574
|
-
requiresConfirmation: closeRecommendations || thinTs
|
|
1649
|
+
requiresConfirmation: closeRecommendations || thinTs,
|
|
1575
1650
|
confirmationReasons: [
|
|
1576
1651
|
...(closeRecommendations ? ['top recommendations are within 2 score points'] : []),
|
|
1577
|
-
...(lowProjectedCoverage ? [`projected governed coverage is ${signals.projectedGovernedCoverage}% (below 90%)`] : []),
|
|
1578
1652
|
...(thinTs ? ['the discovered source surface is thin'] : []),
|
|
1579
1653
|
],
|
|
1580
1654
|
phases: top.phases,
|
|
@@ -1631,7 +1705,7 @@ export function buildArchitectureRecommendation(root, options = {}) {
|
|
|
1631
1705
|
policyPack: policyPackId,
|
|
1632
1706
|
signals: {
|
|
1633
1707
|
sourceFileCount: signals.sourceFileCount,
|
|
1634
|
-
|
|
1708
|
+
discoveredSourceCoverage: signals.discoveredSourceCoverage,
|
|
1635
1709
|
discoveredRoots: signals.discoveredRoots,
|
|
1636
1710
|
packageUnits: signals.repoUnits.map((unit) => ({
|
|
1637
1711
|
root: unit.root,
|
|
@@ -1664,7 +1738,7 @@ export function buildArchitectureRecommendation(root, options = {}) {
|
|
|
1664
1738
|
// a thin slice and can mis-flag framework internals, so steer these to the adoption flow.
|
|
1665
1739
|
mature: signals.sourceFileCount >= MATURE_REPO_FILE_THRESHOLD,
|
|
1666
1740
|
initCommand: `${arkCommand(root, 'ark', `init --archetype ${result.archetype} --yes`)}`,
|
|
1667
|
-
firstCommand: `${arkCommand(root, 'ark', `
|
|
1741
|
+
firstCommand: `${arkCommand(root, 'ark', `start --apply --archetype ${result.archetype}`)}`,
|
|
1668
1742
|
adoptCommand: arkCommand(root, 'ark-check', '--recommend --write-plan'),
|
|
1669
1743
|
recommendCommand: arkCommand(root, 'ark-check', '--recommend'),
|
|
1670
1744
|
checkCommand: arkCommand(root, 'ark-check', '--root . --config ark.config.json --strict-config'),
|
|
@@ -1705,6 +1779,49 @@ export function resolveArchetypePreset(archetypeId, playbookPath = defaultPlaybo
|
|
|
1705
1779
|
};
|
|
1706
1780
|
}
|
|
1707
1781
|
|
|
1782
|
+
/**
|
|
1783
|
+
* Preset `ark start --internal-apply` / `--init` would write. Shared so recommend
|
|
1784
|
+
* can project the same governed % start and doctor measure after that write.
|
|
1785
|
+
*/
|
|
1786
|
+
export function resolveStartInitPreset(root, rec = {}, archetype = rec.archetype) {
|
|
1787
|
+
const presetFromArchetype =
|
|
1788
|
+
archetype && isValidArchetypeId(archetype)
|
|
1789
|
+
? resolveArchetypePreset(archetype).preset
|
|
1790
|
+
: undefined;
|
|
1791
|
+
const preset = rec.preset || presetFromArchetype;
|
|
1792
|
+
const includeRoots = resolveIncludeRoots(root);
|
|
1793
|
+
const tsPackages = detectTsPackageRoots(root);
|
|
1794
|
+
const nestedTsPackages = tsPackages.filter((entry) => entry !== '.');
|
|
1795
|
+
const workspaces = detectWorkspaces(root);
|
|
1796
|
+
const looksLikeMonorepo =
|
|
1797
|
+
includeRoots.length > 0 ||
|
|
1798
|
+
nestedTsPackages.length > 0 ||
|
|
1799
|
+
workspaces.length > 0 ||
|
|
1800
|
+
fs.existsSync(path.join(root, 'rush.json')) ||
|
|
1801
|
+
fs.existsSync(path.join(root, 'pnpm-workspace.yaml')) ||
|
|
1802
|
+
fs.existsSync(path.join(root, 'lerna.json')) ||
|
|
1803
|
+
fs.existsSync(path.join(root, 'apps')) ||
|
|
1804
|
+
fs.existsSync(path.join(root, 'packages'));
|
|
1805
|
+
if (rec.preset === 'vite-vercel-spa' || preset === 'vite-vercel-spa') {
|
|
1806
|
+
return 'vite-vercel-spa';
|
|
1807
|
+
}
|
|
1808
|
+
if (looksLikeMonorepo && (rec.mature || includeRoots.length > 0 || tsPackages.length > 0)) {
|
|
1809
|
+
const useUi =
|
|
1810
|
+
rec.preset === 'feature-sliced' ||
|
|
1811
|
+
rec.archetype === 'frontend-surface' ||
|
|
1812
|
+
(nestedTsPackages.length > 0 && includeRoots.length === 0 && !rec.mature);
|
|
1813
|
+
return useUi && nestedTsPackages.length <= 3 ? 'ui-surface' : 'monorepo';
|
|
1814
|
+
}
|
|
1815
|
+
return preset ?? null;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/** One-minute / preview footer when the coverage·confidence gate may refuse apply. */
|
|
1819
|
+
export const START_APPLY_REFUSE_FOOTER = [
|
|
1820
|
+
'If apply refuses (coverage below 50% or weak shape), that lock is deliberate.',
|
|
1821
|
+
'Lock the shape: --archetype <id> · --preset <name> · --force',
|
|
1822
|
+
'Inspect ranked shapes: arkgate-check --recommend',
|
|
1823
|
+
].join('\n');
|
|
1824
|
+
|
|
1708
1825
|
export function mapWizardChoiceToArchetype(choiceKey) {
|
|
1709
1826
|
const entry = INIT_WIZARD_CHOICES.find((c) => c.key === String(choiceKey).trim());
|
|
1710
1827
|
if (!entry) return null;
|
|
@@ -1753,6 +1870,18 @@ export function formatArchitectureRecommendationHuman(recommendation) {
|
|
|
1753
1870
|
lines.push('');
|
|
1754
1871
|
lines.push(`Archetype: ${recommendation.archetype} — ${recommendation.label}`);
|
|
1755
1872
|
lines.push(`Preset: ${recommendation.preset} (confidence ${recommendation.confidence})`);
|
|
1873
|
+
const projected =
|
|
1874
|
+
recommendation.projectedCoverage?.percent ??
|
|
1875
|
+
recommendation.signals?.projectedGovernedCoverage;
|
|
1876
|
+
if (typeof projected === 'number') {
|
|
1877
|
+
const classified = recommendation.projectedCoverage?.classifiedFiles;
|
|
1878
|
+
const total = recommendation.projectedCoverage?.totalFiles;
|
|
1879
|
+
const counts =
|
|
1880
|
+
typeof classified === 'number' && typeof total === 'number'
|
|
1881
|
+
? ` (${classified}/${total} files)`
|
|
1882
|
+
: '';
|
|
1883
|
+
lines.push(`Projected governed coverage: ${projected}%${counts}`);
|
|
1884
|
+
}
|
|
1756
1885
|
if (recommendation.requiresConfirmation) {
|
|
1757
1886
|
lines.push('⚠ Confirmation required before applying this recommendation.');
|
|
1758
1887
|
for (const reason of recommendation.confirmationReasons ?? []) lines.push(` - ${reason}`);
|
package/bin/ark.mjs
CHANGED
|
@@ -8,10 +8,10 @@ import {
|
|
|
8
8
|
arkCommand,
|
|
9
9
|
buildArchitectureRecommendation,
|
|
10
10
|
detectPackageManager,
|
|
11
|
-
detectWorkspaces,
|
|
12
11
|
evaluateStartShapeConfidenceGate,
|
|
13
12
|
resolveIncludeRoots,
|
|
14
13
|
detectTsPackageRoots,
|
|
14
|
+
resolveStartInitPreset,
|
|
15
15
|
INIT_WIZARD_CHOICES,
|
|
16
16
|
isValidArchetypeId,
|
|
17
17
|
mapWizardChoiceToArchetype,
|
|
@@ -586,41 +586,23 @@ async function start(args) {
|
|
|
586
586
|
const configPath = path.join(root, 'ark.config.json');
|
|
587
587
|
if (!fs.existsSync(configPath)) {
|
|
588
588
|
const initArgs = ['--root', root, '--init'];
|
|
589
|
-
const
|
|
589
|
+
const startPreset = resolveStartInitPreset(root, rec ?? {}, archetype);
|
|
590
590
|
const includeRoots = resolveIncludeRoots(root);
|
|
591
591
|
const tsPackages = detectTsPackageRoots(root);
|
|
592
592
|
const nestedTsPackages = tsPackages.filter((entry) => entry !== '.');
|
|
593
|
-
|
|
594
|
-
const looksLikeMonorepo =
|
|
595
|
-
includeRoots.length > 0 ||
|
|
596
|
-
nestedTsPackages.length > 0 ||
|
|
597
|
-
workspaces.length > 0 ||
|
|
598
|
-
fs.existsSync(path.join(root, 'rush.json')) ||
|
|
599
|
-
fs.existsSync(path.join(root, 'pnpm-workspace.yaml')) ||
|
|
600
|
-
fs.existsSync(path.join(root, 'lerna.json')) ||
|
|
601
|
-
fs.existsSync(path.join(root, 'apps')) ||
|
|
602
|
-
fs.existsSync(path.join(root, 'packages'));
|
|
603
|
-
// SPA (Vite + root api/lib) wins over monorepo heuristics (NEW-SPA-DEFAULT-LAYOUT).
|
|
604
|
-
if (rec?.preset === 'vite-vercel-spa' || preset === 'vite-vercel-spa') {
|
|
593
|
+
if (startPreset === 'vite-vercel-spa') {
|
|
605
594
|
initArgs.push('--preset', 'vite-vercel-spa');
|
|
606
595
|
console.log(' Vite/Vercel SPA layout detected — include src,api,lib; api→Application; db clients→Persistence.');
|
|
607
|
-
} else if (
|
|
608
|
-
|
|
609
|
-
// UI-heavy TS packages (Remotion/Vite) prefer ui-surface patterns when recommend says so.
|
|
610
|
-
const useUi =
|
|
611
|
-
rec?.preset === 'feature-sliced' ||
|
|
612
|
-
rec?.archetype === 'frontend-surface' ||
|
|
613
|
-
(nestedTsPackages.length > 0 && includeRoots.length === 0 && !rec?.mature);
|
|
614
|
-
initArgs.push('--preset', useUi && nestedTsPackages.length <= 3 ? 'ui-surface' : 'monorepo');
|
|
596
|
+
} else if (startPreset === 'monorepo' || startPreset === 'ui-surface') {
|
|
597
|
+
initArgs.push('--preset', startPreset);
|
|
615
598
|
const shown = includeRoots.length > 0 ? includeRoots : nestedTsPackages;
|
|
616
599
|
console.log(
|
|
617
600
|
shown.length > 0
|
|
618
601
|
? ` Multi-package / TS package layout detected — profile include: ${shown.join(', ')}.`
|
|
619
602
|
: ' Multi-package layout detected — using monorepo profile.'
|
|
620
603
|
);
|
|
621
|
-
} else if (
|
|
622
|
-
|
|
623
|
-
initArgs.push('--preset', preset);
|
|
604
|
+
} else if (startPreset) {
|
|
605
|
+
initArgs.push('--preset', startPreset);
|
|
624
606
|
}
|
|
625
607
|
const status = runArkCheck(initArgs, { cwd: root });
|
|
626
608
|
if (status !== 0) return status;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Soft ADR / decision-note presence when gates are demanded.
|
|
3
|
+
* Tooling I/O. Never a gate fail. Absence is silent unless require-gates
|
|
4
|
+
* or adopted-strict (required-merge) is on.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
/** Conventional homes a human already uses. First hit wins. No config key. */
|
|
11
|
+
export const ADR_PRESENCE_HOMES = Object.freeze([
|
|
12
|
+
'docs/adr',
|
|
13
|
+
'docs/decisions',
|
|
14
|
+
'docs/adr.md',
|
|
15
|
+
'docs/decision-log.md',
|
|
16
|
+
'ADR.md',
|
|
17
|
+
'DECISIONS.md',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export const ADR_PRESENCE_ASK =
|
|
21
|
+
'Gates are required here, but there is no short decision note yet.';
|
|
22
|
+
|
|
23
|
+
export const ADR_PRESENCE_NEXT =
|
|
24
|
+
'Add a short note under docs/adr/ (or docs/decisions/) when you loosen a rule or add a real gate. Not every change.';
|
|
25
|
+
|
|
26
|
+
function isNonEmptyMarkdownFile(file) {
|
|
27
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return false;
|
|
28
|
+
try {
|
|
29
|
+
return fs.readFileSync(file, 'utf8').trim().length > 0;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function directoryHasMarkdown(dir) {
|
|
36
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return false;
|
|
37
|
+
try {
|
|
38
|
+
return fs.readdirSync(dir).some((name) => {
|
|
39
|
+
if (!name.toLowerCase().endsWith('.md')) return false;
|
|
40
|
+
return isNonEmptyMarkdownFile(path.join(dir, name));
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* First conventional decision-note home that already has a note.
|
|
49
|
+
* Empty folders and empty files do not count.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} root
|
|
52
|
+
* @returns {string | null}
|
|
53
|
+
*/
|
|
54
|
+
export function findAdrPresenceHome(root) {
|
|
55
|
+
if (typeof root !== 'string' || root.length === 0) return null;
|
|
56
|
+
for (const rel of ADR_PRESENCE_HOMES) {
|
|
57
|
+
const abs = path.join(root, rel);
|
|
58
|
+
if (rel.endsWith('.md')) {
|
|
59
|
+
if (isNonEmptyMarkdownFile(abs)) return rel;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (directoryHasMarkdown(abs)) return rel;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Soft residual when gates are demanded and no decision-note home exists.
|
|
69
|
+
* `demanded` is --require-gates / --strict / --strict-merge, or doctor
|
|
70
|
+
* adopted === 'required-merge'. Off → null (silent).
|
|
71
|
+
*
|
|
72
|
+
* @param {{ root?: string, demanded?: boolean }} [input]
|
|
73
|
+
* @returns {{ missing: true, ask: string, nextAction: string } | null}
|
|
74
|
+
*/
|
|
75
|
+
export function collectAdrPresenceResidual(input = {}) {
|
|
76
|
+
if (input.demanded !== true) return null;
|
|
77
|
+
const home = findAdrPresenceHome(input.root ?? '');
|
|
78
|
+
if (home) return null;
|
|
79
|
+
return {
|
|
80
|
+
missing: true,
|
|
81
|
+
ask: ADR_PRESENCE_ASK,
|
|
82
|
+
nextAction: ADR_PRESENCE_NEXT,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Human hint for --require-gates. Never changes exit code.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} root
|
|
90
|
+
* @param {(line: string) => void} write
|
|
91
|
+
*/
|
|
92
|
+
export function printAdrPresenceHint(root, write) {
|
|
93
|
+
const residual = collectAdrPresenceResidual({ root, demanded: true });
|
|
94
|
+
if (!residual || typeof write !== 'function') return;
|
|
95
|
+
write(residual.ask);
|
|
96
|
+
write(`Next: ${residual.nextAction}`);
|
|
97
|
+
}
|
|
@@ -105,6 +105,8 @@ export function buildAgentProjectionBody(facts) {
|
|
|
105
105
|
'',
|
|
106
106
|
AGENT_PROJECTION_NON_ENFORCEMENT_LABEL,
|
|
107
107
|
'',
|
|
108
|
+
'Contener · Guiar · Ordenar — contain the write, guide the next step, order leftover mess. Skills never enforce.',
|
|
109
|
+
'',
|
|
108
110
|
`- **arkgateVersion:** \`${version}\` (must match the installed package; regenerate with \`ark agents-md --write\` after upgrade)`,
|
|
109
111
|
`- **projectionSchema:** \`${ARK_AGENT_PROJECTION_SCHEMA_VERSION}\``,
|
|
110
112
|
`- **profile:** \`${profile}\`${host ? ` · **host:** \`${host}\`` : ''}`,
|