arkgate 4.6.5 → 4.6.7
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 +72 -2106
- package/README.md +11 -9
- package/bin/ark-check-runtime.mjs +36 -332
- package/bin/ark-mcp-runtime.mjs +7 -323
- package/bin/ark-shared.mjs +24 -158
- package/bin/ark.mjs +13 -3
- package/bin/lib/adoption-stance.mjs +104 -0
- package/bin/lib/check-args.mjs +173 -0
- package/bin/lib/check-config-detect.mjs +101 -0
- package/bin/lib/check-watch.mjs +80 -0
- package/bin/lib/ci-merge-boundary.mjs +4 -2
- package/bin/lib/deep-module-coach.mjs +3 -0
- package/bin/lib/design-delta.mjs +2 -2
- package/bin/lib/design-smells.mjs +1 -1
- package/bin/lib/diagnostic-catalog.mjs +1 -1
- package/bin/lib/doctor-advisories.mjs +2 -2
- package/bin/lib/doctor-human.mjs +509 -0
- package/bin/lib/doctor-next-actions.mjs +20 -2
- package/bin/lib/doctor-plan.mjs +86 -456
- package/bin/lib/enforcement-honesty.mjs +70 -0
- package/bin/lib/first-run-help.mjs +8 -7
- package/bin/lib/github-enforcement.mjs +22 -9
- package/bin/lib/html-report-advisories.mjs +10 -2
- package/bin/lib/html-report.mjs +26 -9
- package/bin/lib/mcp-adoption.mjs +19 -0
- package/bin/lib/mcp-hook-payload.mjs +328 -0
- package/bin/lib/package-manager.mjs +174 -0
- package/bin/lib/policy-delta-io.mjs +5 -1
- package/bin/lib/post-green-path.mjs +5 -1
- package/bin/lib/product-copy.mjs +6 -3
- 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 +66 -2
- package/bin/lib/team-parliament.mjs +25 -5
- package/bin/lib/unavailable-analysis.mjs +1 -0
- package/dist/index.cjs +2 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +2 -2
- package/docs/README.md +6 -10
- package/docs/ai-gates.md +12 -5
- package/docs/configuration.md +9 -1
- package/docs/diagnostics.md +2 -2
- 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.design-delta.schema.json +1 -1
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -12,12 +12,10 @@ import {
|
|
|
12
12
|
import * as arkShared from '../ark-shared.mjs';
|
|
13
13
|
import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
|
|
14
14
|
import { describePackageVersionDualTruth } from './field-install.mjs';
|
|
15
|
-
import {
|
|
16
|
-
detectAgentHomeGaps,
|
|
17
|
-
agentHomeConcernIsActive,
|
|
18
|
-
} from './agent-homes.mjs';
|
|
19
|
-
import { operatingModeTitle } from './product-copy.mjs';
|
|
15
|
+
import { detectAgentHomeGaps } from './agent-homes.mjs';
|
|
20
16
|
import { collectDoctorNextActions } from './doctor-next-actions.mjs';
|
|
17
|
+
import { printDoctorCompactHuman, printDoctorDetailsHuman } from './doctor-human.mjs';
|
|
18
|
+
export { printDoctorCompactHuman, printDoctorDetailsHuman };
|
|
21
19
|
export { summarizeRulesUnderContract };
|
|
22
20
|
|
|
23
21
|
/** Optional S3 dual-match classifier when ark-shared exports it (soft dep for S5 landing). */
|
|
@@ -28,7 +26,6 @@ const matchingLayersForRelativePath =
|
|
|
28
26
|
import {
|
|
29
27
|
collectAdoptionGaps,
|
|
30
28
|
detectSkillGaps,
|
|
31
|
-
skillGapsForActiveHost,
|
|
32
29
|
detectCodexHomeGap,
|
|
33
30
|
codexConcernIsActive,
|
|
34
31
|
detectWritePathCapabilities,
|
|
@@ -50,7 +47,6 @@ import {
|
|
|
50
47
|
} from './design-smells.mjs';
|
|
51
48
|
import {
|
|
52
49
|
buildPostGreenNextAction,
|
|
53
|
-
isDoctorHealthyNothingToDo,
|
|
54
50
|
DESIGN_WEAK_HONESTY_FLAGS,
|
|
55
51
|
} from './post-green-path.mjs';
|
|
56
52
|
import {
|
|
@@ -63,19 +59,17 @@ import {
|
|
|
63
59
|
summarizeGoldenPattern,
|
|
64
60
|
} from './golden-pattern.mjs';
|
|
65
61
|
import { summarizePilotLoop } from './pilot-loop.mjs';
|
|
66
|
-
import { computeDoctorAdvisories
|
|
62
|
+
import { computeDoctorAdvisories } from './doctor-advisories.mjs';
|
|
67
63
|
import { ANALYSIS_COMPLETENESS, analysisIncompleteStatement, normalizeAnalysisCompleteness } from './analysis-completeness.mjs';
|
|
68
|
-
import {
|
|
69
|
-
import {
|
|
70
|
-
import {
|
|
71
|
-
buildDoctorImprovementCompass,
|
|
72
|
-
printImprovementCompassSection,
|
|
73
|
-
} from './improvement-compass-doctor.mjs';
|
|
74
|
-
import {
|
|
75
|
-
buildDeepModuleCoachAdvisory,
|
|
76
|
-
printDeepModuleCoachSection,
|
|
77
|
-
} from './deep-module-coach.mjs';
|
|
64
|
+
import { buildDoctorImprovementCompass } from './improvement-compass-doctor.mjs';
|
|
65
|
+
import { buildDeepModuleCoachAdvisory } from './deep-module-coach.mjs';
|
|
78
66
|
import { writeCiMergeBoundary } from './ci-merge-boundary.mjs';
|
|
67
|
+
import {
|
|
68
|
+
classifyAdopted,
|
|
69
|
+
githubEvidenceForCiMergeBoundary,
|
|
70
|
+
readAdoptionStance,
|
|
71
|
+
NOT_ADOPTED_NEXT_ACTION,
|
|
72
|
+
} from './adoption-stance.mjs';
|
|
79
73
|
|
|
80
74
|
const color = {
|
|
81
75
|
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
@@ -540,7 +534,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
540
534
|
try {
|
|
541
535
|
ciMergeBoundary = writeCiMergeBoundary(root, {
|
|
542
536
|
writePath,
|
|
543
|
-
github: adoption
|
|
537
|
+
github: githubEvidenceForCiMergeBoundary(adoption, writePath),
|
|
544
538
|
});
|
|
545
539
|
} catch {
|
|
546
540
|
ciMergeBoundary = null;
|
|
@@ -631,6 +625,13 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
631
625
|
: packageVersionTruth?.code === 'PACKAGE_PIN_ABSENT'
|
|
632
626
|
? 'Add arkgate to package.json and install so CI/npx resolve this CLI (PACKAGE_PIN_ABSENT)'
|
|
633
627
|
: null;
|
|
628
|
+
const stanceFile = readAdoptionStance(root);
|
|
629
|
+
const githubForBoundary = githubEvidenceForCiMergeBoundary(adoption, writePath);
|
|
630
|
+
const adopted = classifyAdopted({
|
|
631
|
+
stance: stanceFile,
|
|
632
|
+
github: githubForBoundary,
|
|
633
|
+
ci: ciMergeBoundary?.ci,
|
|
634
|
+
});
|
|
634
635
|
const { coverageHonesty, baselineHonesty, writePathHonesty, productHonesty } =
|
|
635
636
|
computeDoctorEnforcementHonesty({
|
|
636
637
|
governedPercent: cov.governed.percent,
|
|
@@ -656,12 +657,18 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
656
657
|
: rulesUnderContract?.active === true
|
|
657
658
|
? { active: true, extraMergeTeeth: false }
|
|
658
659
|
: null,
|
|
659
|
-
primaryNextAction:
|
|
660
|
+
primaryNextAction:
|
|
661
|
+
adopted === 'not-adopted' ? NOT_ADOPTED_NEXT_ACTION : postGreenPath?.action ?? dualTruthNext,
|
|
660
662
|
operatingMode,
|
|
661
663
|
packageInstalled,
|
|
662
664
|
selfHost:
|
|
663
665
|
packageVersionTruth?.selfHost === true ||
|
|
664
666
|
packageVersionTruth?.code === 'PACKAGE_PIN_SELF_HOST',
|
|
667
|
+
adopted,
|
|
668
|
+
ciMergeBoundary,
|
|
669
|
+
github: githubForBoundary,
|
|
670
|
+
adoptionStance: stanceFile,
|
|
671
|
+
stewardNudge: doctorAdvisories.stewardNudge,
|
|
665
672
|
});
|
|
666
673
|
|
|
667
674
|
// Improvement compass: projection only — never feeds ok/valid/goal.met.
|
|
@@ -687,6 +694,11 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
687
694
|
improvementCompass,
|
|
688
695
|
pilotLoop,
|
|
689
696
|
});
|
|
697
|
+
const stewardUnfinished = Boolean(
|
|
698
|
+
doctorAdvisories.stewardNudge?.emptyStewardsPastGrace ||
|
|
699
|
+
(doctorAdvisories.stewardNudge?.needsStewards &&
|
|
700
|
+
(doctorAdvisories.stewardNudge?.stewardCount ?? 0) === 0)
|
|
701
|
+
);
|
|
690
702
|
|
|
691
703
|
if (asJson) {
|
|
692
704
|
(options.writeJson ?? console.log)(
|
|
@@ -711,11 +723,20 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
711
723
|
...(options.designDelta ? { designDelta: options.designDelta } : {}),
|
|
712
724
|
// Q01: primary next action when Shape residual dominates (null if not design-weak).
|
|
713
725
|
postGreenPath,
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
726
|
+
adoptionStance: adopted,
|
|
727
|
+
...(adopted === 'not-adopted'
|
|
728
|
+
? {
|
|
729
|
+
primaryNextAction: NOT_ADOPTED_NEXT_ACTION,
|
|
730
|
+
...(postGreenPath ? DESIGN_WEAK_HONESTY_FLAGS : {}),
|
|
731
|
+
}
|
|
732
|
+
: postGreenPath
|
|
733
|
+
? { primaryNextAction: postGreenPath.action, ...DESIGN_WEAK_HONESTY_FLAGS }
|
|
734
|
+
: productHonesty.primaryNextAction
|
|
735
|
+
? { primaryNextAction: productHonesty.primaryNextAction }
|
|
736
|
+
: {}),
|
|
737
|
+
...(stewardUnfinished
|
|
738
|
+
? { healthyFinishedForbidden: true, stewardsUnfinished: true }
|
|
739
|
+
: {}),
|
|
719
740
|
// Q03: advisory golden for new-code placement (absent = no claim).
|
|
720
741
|
goldenPattern,
|
|
721
742
|
// Y06: advisory pure-layer opt-in (null when not applicable).
|
|
@@ -834,48 +855,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
834
855
|
return;
|
|
835
856
|
}
|
|
836
857
|
|
|
837
|
-
const ok = color.green('✓');
|
|
838
|
-
const warn = color.yellow('!');
|
|
839
|
-
const bad = color.red('✗');
|
|
840
|
-
const line = (mark, text) => console.log(` ${mark} ${text}`);
|
|
841
|
-
console.log(color.bold(`Ark doctor — ${path.basename(path.resolve(root)) || '.'}`));
|
|
842
|
-
if (!analysisComplete) line(warn, analysisIncompleteStatement(completeness));
|
|
843
|
-
|
|
844
858
|
const emptyScope = emptyScopeEarly;
|
|
845
|
-
const mode = operatingMode;
|
|
846
|
-
console.log('');
|
|
847
|
-
console.log(color.bold('Operating mode'));
|
|
848
|
-
// Modes are detected states, not user-picked settings. Plain-language "what you do next".
|
|
849
|
-
// Never paint green (ok) under design residual — edges clean ≠ design done (product-voice).
|
|
850
|
-
const modeMark =
|
|
851
|
-
mode === 'enforce' && !designFitness.designWeak
|
|
852
|
-
? ok
|
|
853
|
-
: warn;
|
|
854
|
-
// modeTitle alone names the light — bodies must not re-prefix Suggest/Adapt/Enforce.
|
|
855
|
-
const modeHelp = {
|
|
856
|
-
suggest:
|
|
857
|
-
'thin or new tree; architecture config is not yet in charge. You do not pick this light. Next: ark start (preview), then ark start --apply; re-check with --doctor.',
|
|
858
|
-
adapt:
|
|
859
|
-
'config and tree still disagree, or debt is open. The write path does not fully protect you yet. You do not pick this light. Next: do doctor top action #1 (often /ark-adopt or /ark-autopilot).',
|
|
860
|
-
enforce:
|
|
861
|
-
'honest coverage and clean checked imports. You arrived here; you never turn Enforce on. Next: keep the host write path and CI check; only NEW violations should fail.',
|
|
862
|
-
};
|
|
863
|
-
const modeTitle = operatingModeTitle(mode, designFitness.designWeak);
|
|
864
|
-
line(
|
|
865
|
-
modeMark,
|
|
866
|
-
`${modeTitle} — ${
|
|
867
|
-
designFitness.designWeak
|
|
868
|
-
? 'import rules check out; design smells remain. Green is not elegant design. You do not pick this light. Next: one Shape door — /ark-explore shape-focus → plan B; apply B only with /ark-autopilot and your OK. A clean import check is not done.'
|
|
869
|
-
: modeHelp[mode]
|
|
870
|
-
}`
|
|
871
|
-
);
|
|
872
|
-
if (emptyScope) {
|
|
873
|
-
line(
|
|
874
|
-
bad,
|
|
875
|
-
'Empty scope: include paths match 0 source files — a green check is meaningless until include/layers match the tree (monorepo → apps/packages, or /ark-adopt).'
|
|
876
|
-
);
|
|
877
|
-
}
|
|
878
|
-
|
|
879
859
|
const safetyHasEntries = Boolean(
|
|
880
860
|
options.safety &&
|
|
881
861
|
[
|
|
@@ -911,396 +891,46 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
911
891
|
showNewHere,
|
|
912
892
|
designFitness,
|
|
913
893
|
operatingMode,
|
|
894
|
+
adopted,
|
|
895
|
+
stewardNudge: doctorAdvisories.stewardNudge,
|
|
914
896
|
});
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
' ',
|
|
951
|
-
color.dim(
|
|
952
|
-
`merge planes: ${rulesUnderContract.mergePlanes.failMergeWhen} · ${rulesUnderContract.mergePlanes.dualPlaneStamp}`
|
|
953
|
-
)
|
|
954
|
-
);
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
printImprovementCompassSection(improvementCompass, { line, warn, ok, color });
|
|
959
|
-
printDeepModuleCoachSection(deepModuleCoach, { line, warn, ok, color });
|
|
960
|
-
|
|
961
|
-
console.log('');
|
|
962
|
-
console.log(color.bold('Design fitness'));
|
|
963
|
-
if (designSmells.length === 0) {
|
|
964
|
-
line(analysisComplete ? ok : warn, designFitness.label);
|
|
965
|
-
} else {
|
|
966
|
-
line(designFitness.designWeak ? warn : warn, designFitness.label);
|
|
967
|
-
for (const smell of designSmells.slice(0, 5)) {
|
|
968
|
-
// Q02: outcome-first (plain language); technical message stays in JSON + dim detail.
|
|
969
|
-
const outcome = smell.outcome || smell.message;
|
|
970
|
-
line(' ', `[${smell.id}] ${outcome}`);
|
|
971
|
-
if (smell.outcome && smell.message && smell.message !== smell.outcome) {
|
|
972
|
-
line(' ', color.dim(`detail: ${smell.message}`));
|
|
973
|
-
}
|
|
974
|
-
if (smell.evidence?.length) {
|
|
975
|
-
line(' ', color.dim(`evidence: ${smell.evidence.slice(0, 4).join(', ')}`));
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
// Q04 — surface one next pilot under leftover design work.
|
|
979
|
-
if (pilotLoop?.active && pilotLoop.nextPilot) {
|
|
980
|
-
const np = pilotLoop.nextPilot;
|
|
981
|
-
line(
|
|
982
|
-
warn,
|
|
983
|
-
`Next pilot (one at a time): ${np.pilotTarget || np.pilot} [${np.smellId}] → re-doctor after change`
|
|
984
|
-
);
|
|
985
|
-
line(' ', color.dim(`success: ${np.successSignal}`));
|
|
986
|
-
line(' ', color.dim('never multi-pilot batch; pattern bets are never auto-applied'));
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
if (options.designDelta) {
|
|
991
|
-
console.log('');
|
|
992
|
-
console.log(color.bold('Design delta (opt-in)'));
|
|
993
|
-
for (const row of designDeltaDoctorLines(options.designDelta))
|
|
994
|
-
line(row.level === 'bad' ? bad : row.level === 'ok' ? ok : ' ', row.level === 'dim' ? color.dim(row.text) : row.text);
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
// Q03 — optional golden pattern note (advisory for new code only).
|
|
998
|
-
if (goldenPattern.present) {
|
|
999
|
-
console.log('');
|
|
1000
|
-
console.log(color.bold('Golden pattern (new code)'));
|
|
1001
|
-
line(
|
|
1002
|
-
ok,
|
|
1003
|
-
`"${goldenPattern.name}" — ${goldenPattern.norm}` +
|
|
1004
|
-
(goldenPattern.newCodeHome ? ` Prefer: ${goldenPattern.newCodeHome}.` : '') +
|
|
1005
|
-
' Advisory only — does not clear leftover design work or replace the gate.'
|
|
1006
|
-
);
|
|
1007
|
-
} else if (goldenPattern.invalid) {
|
|
1008
|
-
console.log('');
|
|
1009
|
-
console.log(color.bold('Golden pattern (new code)'));
|
|
1010
|
-
line(
|
|
1011
|
-
warn,
|
|
1012
|
-
`${goldenPattern.path} is present but invalid (${goldenPattern.error || 'invalid'}). ` +
|
|
1013
|
-
'Fix or remove it — absence is fine; a bad file is not guidance.'
|
|
1014
|
-
);
|
|
1015
|
-
}
|
|
1016
|
-
// Y06 — one-line pure-layer opt-in (U05 voice; never blocker).
|
|
1017
|
-
if (pureLayerOptIn) {
|
|
1018
|
-
line(' ', color.dim(pureLayerOptIn.message));
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
printDoctorAdvisories(doctorAdvisories, { line, warn, color }); // advisory sections
|
|
1022
|
-
|
|
1023
|
-
console.log('');
|
|
1024
|
-
console.log(color.bold('Coverage'));
|
|
1025
|
-
const govMark =
|
|
1026
|
-
emptyScope || cov.governed.percent < 50
|
|
1027
|
-
? bad
|
|
1028
|
-
: cov.governed.percent >= 80
|
|
1029
|
-
? ok
|
|
1030
|
-
: warn;
|
|
1031
|
-
line(govMark, `Governed: ${cov.governed.percent}% (${cov.governed.classifiedFiles}/${cov.governed.totalFiles} files)`);
|
|
1032
|
-
if (coverageHonesty.greenIsNotEnforcement) {
|
|
1033
|
-
line(coverageHonesty.worseThanNoGate ? bad : warn, coverageHonesty.message);
|
|
1034
|
-
}
|
|
1035
|
-
if (cov.suggestions.length > 0) {
|
|
1036
|
-
line(warn, `${cov.suggestions.length} ungoverned director(y/ies) — proposals: ${arkCommand(root, 'ark-check', '--coverage')}`);
|
|
1037
|
-
}
|
|
1038
|
-
if (cov.emptyLayers.length > 0) line(warn, `Empty layers (pattern matches nothing): ${cov.emptyLayers.join(', ')}`);
|
|
1039
|
-
if (cov.layersWithoutRules.length > 0) line(warn, `Layers with no rule edge: ${cov.layersWithoutRules.join(', ')}`);
|
|
1040
|
-
if (cov.dualMembership?.count > 0) {
|
|
1041
|
-
line(
|
|
1042
|
-
warn,
|
|
1043
|
-
`Dual-match: ${cov.dualMembership.count} file(s) match multiple layers — ${cov.dualMembership.note ?? 'review overlapping globs'}`
|
|
1044
|
-
);
|
|
1045
|
-
}
|
|
1046
|
-
if (cov.suggestions.length === 0 && cov.emptyLayers.length === 0) line(ok, 'Every layer classifies files; no empty layers');
|
|
1047
|
-
|
|
1048
|
-
if (packageVersionTruth?.dualTruth) {
|
|
1049
|
-
console.log('');
|
|
1050
|
-
console.log(color.bold('Package pin (dual-truth)'));
|
|
1051
|
-
line(warn, packageVersionTruth.note);
|
|
1052
|
-
} else if (packageVersionTruth?.code === 'PACKAGE_PIN_ABSENT') {
|
|
1053
|
-
console.log('');
|
|
1054
|
-
console.log(color.bold('Package pin'));
|
|
1055
|
-
line(warn, packageVersionTruth.note);
|
|
1056
|
-
}
|
|
1057
|
-
if (options.configWalkedUp && options.configRoot) {
|
|
1058
|
-
line(
|
|
1059
|
-
ok,
|
|
1060
|
-
`Config walk-up: using monorepo root ${options.configRoot} (ark.config.json not in cwd package)`
|
|
1061
|
-
);
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
if (showNewHere) {
|
|
1065
|
-
console.log('');
|
|
1066
|
-
console.log(color.bold('New here?'));
|
|
1067
|
-
// Suggest residual: start → doctor only (not a competing recommend/architect curriculum).
|
|
1068
|
-
line(ok, `Primary path: ${arkCommand(root, 'ark', 'start')} (preview) → ${arkCommand(root, 'ark', 'start --apply')} → re-run --doctor`);
|
|
1069
|
-
if (recommendation) {
|
|
1070
|
-
line(warn, `Sensor shape hint (not a second curriculum): ${recommendation.archetype} — ${recommendation.label} (preset ${recommendation.preset})`);
|
|
1071
|
-
if (recommendation.galleryStarter) line(ok, `Gallery starter (optional): ${recommendation.galleryStarter}`);
|
|
1072
|
-
if (recommendation.policyPack) {
|
|
1073
|
-
line(ok, `Policy pack (optional expert): ${arkCommand(root, 'ark-check', `--apply-policy-pack ${recommendation.policyPack}`)}`);
|
|
1074
|
-
}
|
|
1075
|
-
if (recommendation.signals?.nestFramework) {
|
|
1076
|
-
line(ok, 'Nest modular monolith → prefer hexagonal (or ddd-bounded-contexts if you have src/contexts/*)');
|
|
1077
|
-
}
|
|
1078
|
-
if (recommendation.signals?.monorepoTooling?.length) {
|
|
1079
|
-
line(ok, `Monorepo tooling (${recommendation.signals.monorepoTooling.join(', ')}) → preset monorepo (apps/packages/libs)`);
|
|
1080
|
-
}
|
|
1081
|
-
} else {
|
|
1082
|
-
line(warn, 'Low governed coverage or fresh config — finish start, then re-run doctor before adding layers of code.');
|
|
1083
|
-
}
|
|
1084
|
-
line(ok, `Optional sensor detail: ${arkCommand(root, 'ark-check', '--recommend')}`);
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
console.log('');
|
|
1088
|
-
console.log(color.bold('Violations'));
|
|
1089
|
-
if (violations.length === 0) {
|
|
1090
|
-
if (!analysisComplete) line(warn, 'No reported violations — contract compliance is not verified until analysis is complete');
|
|
1091
|
-
else if (emptyScope || cov.governed.percent < 50) {
|
|
1092
|
-
line(
|
|
1093
|
-
warn,
|
|
1094
|
-
'No active violations — coverage is still thin, so green is not yet honest enforcement'
|
|
1095
|
-
);
|
|
1096
|
-
} else if (designFitness.designWeak) {
|
|
1097
|
-
line(warn, `None on checked imports — import rules match the config; leftover design work remains (${modeTitle}). Not healthy finished.`);
|
|
1098
|
-
} else {
|
|
1099
|
-
line(ok, 'None — the code matches the contract on checked edges');
|
|
1100
|
-
}
|
|
1101
|
-
} else {
|
|
1102
|
-
const typeNote = summary.typeOnlyCount > 0 ? ` (${summary.valueCount} value · ${summary.typeOnlyCount} type-only)` : '';
|
|
1103
|
-
const supNote = suppressed > 0 ? `, ${suppressed} frozen` : '';
|
|
1104
|
-
line(
|
|
1105
|
-
activeCount > 0 ? warn : ok,
|
|
1106
|
-
`${violations.length} total${typeNote}${supNote}${activeCount > 0 ? ` — ${activeCount} NOT baselined` : ''}`
|
|
1107
|
-
);
|
|
1108
|
-
for (const edge of summary.edges.slice(0, 3)) line(' ', color.dim(`${edge.count} ${edge.edge}`));
|
|
1109
|
-
if (summary.concentrated) {
|
|
1110
|
-
line(warn, color.dim(`${Math.round(summary.dominantShare * 100)}% on one edge (${summary.dominant}) — likely a contract fix, not debt`));
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
console.log('');
|
|
1115
|
-
console.log(color.bold('Write path (agent)'));
|
|
1116
|
-
const capabilities = writePath.capabilities;
|
|
1117
|
-
const writePathLabels = {
|
|
1118
|
-
repair: 'repair-capable — hard block + machine-readable autoPatch / ARK_REPAIR_JSON',
|
|
1119
|
-
'reject-only': 'reject-only — hard block with prose; no repair payload',
|
|
1120
|
-
'mcp-only': 'MCP tools only — prepare-write/autoPatch available; no PreToolUse hook',
|
|
1121
|
-
none: 'no write gate hook and no Ark MCP',
|
|
897
|
+
const humanView = {
|
|
898
|
+
root,
|
|
899
|
+
analysisComplete,
|
|
900
|
+
completeness,
|
|
901
|
+
doctorAdvisories,
|
|
902
|
+
operatingMode,
|
|
903
|
+
designFitness,
|
|
904
|
+
adopted,
|
|
905
|
+
stewardUnfinished,
|
|
906
|
+
emptyScope,
|
|
907
|
+
options,
|
|
908
|
+
uniqueActions,
|
|
909
|
+
ciMergeBoundary,
|
|
910
|
+
cov,
|
|
911
|
+
writePath,
|
|
912
|
+
writePathHonesty,
|
|
913
|
+
gatesMissing,
|
|
914
|
+
violations,
|
|
915
|
+
coverageHonesty,
|
|
916
|
+
packageVersionTruth,
|
|
917
|
+
designSmells,
|
|
918
|
+
pilotLoop,
|
|
919
|
+
goldenPattern,
|
|
920
|
+
pureLayerOptIn,
|
|
921
|
+
summary,
|
|
922
|
+
suppressed,
|
|
923
|
+
activeCount,
|
|
924
|
+
skillGaps,
|
|
925
|
+
agentHomeGaps,
|
|
926
|
+
baseline,
|
|
927
|
+
baselineHonesty,
|
|
928
|
+
staleBaseline,
|
|
929
|
+
staleRunners,
|
|
930
|
+
adoption,
|
|
931
|
+
color,
|
|
1122
932
|
};
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
? ok
|
|
1126
|
-
: capabilities['advisory-write'] || capabilities['merge-gate']
|
|
1127
|
-
? warn
|
|
1128
|
-
: bad;
|
|
1129
|
-
line(' ', `Active host: ${writePath.activeHost}`);
|
|
1130
|
-
line(' ', `Supported profile: ${writePath.supportSummary}`);
|
|
1131
|
-
line(wpMark, `Mode: ${writePath.mode} — ${writePathLabels[writePath.mode] || writePath.mode}`);
|
|
1132
|
-
if (writePathHonesty.message) line(warn, writePathHonesty.message);
|
|
1133
|
-
if (writePath.sessionNote) {
|
|
1134
|
-
line(warn, writePath.sessionNote);
|
|
1135
|
-
}
|
|
1136
|
-
const enforcement = writePath.enforcementState;
|
|
1137
|
-
for (const row of enforcementDoctorLines(enforcement)) line(row.level === 'ok' ? ok : row.level === 'bad' ? bad : warn, row.text);
|
|
1138
|
-
// EH07 Repair: use support/matrix caps (inventory omits envelope-emitted on Codex).
|
|
1139
|
-
const supportCaps = writePath.support?.capabilities || {};
|
|
1140
|
-
const repairReinjection = supportCaps['repair-reinjection-guaranteed'] === true;
|
|
1141
|
-
const repairEnvelope = supportCaps['repair-envelope-emitted'] === true || supportCaps['repair-payload'] === true;
|
|
1142
|
-
line(
|
|
1143
|
-
repairReinjection ? ok : warn,
|
|
1144
|
-
repairReinjection
|
|
1145
|
-
? 'Repair: envelope + reinjection guaranteed on hard path when installed + trusted'
|
|
1146
|
-
: repairEnvelope
|
|
1147
|
-
? 'Repair: envelope may emit (`--hook-repair`); reinjection not guaranteed (advisory host)'
|
|
1148
|
-
: 'Repair: no hard-boundary payload'
|
|
1149
|
-
);
|
|
1150
|
-
if (writePath.gap) {
|
|
1151
|
-
line(writePath.gap.severity === 'warn' ? warn : warn, writePath.gap.message);
|
|
1152
|
-
if (writePath.gap.fix) {
|
|
1153
|
-
line(' ', color.dim(`Fix: ${writePath.gap.fix}`));
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
console.log('');
|
|
1158
|
-
console.log(color.bold('Gates & skills'));
|
|
1159
|
-
if (gatesMissing.length === 0) line(ok, 'Shared gate artifacts found on disk (AGENTS.md, .mcp.json, CI); runtime activation is reported separately');
|
|
1160
|
-
else {
|
|
1161
|
-
line(bad, `Missing gates: ${gatesMissing.join(', ')}`);
|
|
1162
|
-
}
|
|
1163
|
-
const humanSkillGaps = skillGapsForActiveHost(skillGaps);
|
|
1164
|
-
const legacyCodex = humanSkillGaps.some((g) => g.tool === 'codex' && g.legacyPromptsOnly);
|
|
1165
|
-
const codexLegacySafeDelete = humanSkillGaps.some(
|
|
1166
|
-
(g) => g.tool === 'codex' && g.legacyAdvisory && g.catalogComplete
|
|
1167
|
-
);
|
|
1168
|
-
const remainingGaps = humanSkillGaps.filter(
|
|
1169
|
-
(g) => !(g.tool === 'codex' && (g.legacyPromptsOnly || g.legacyAdvisory))
|
|
1170
|
-
);
|
|
1171
|
-
const remMiss = remainingGaps.reduce((s, g) => s + g.missing, 0);
|
|
1172
|
-
const remStale = remainingGaps.reduce((s, g) => s + g.stale, 0);
|
|
1173
|
-
if (remMiss + remStale === 0 && !legacyCodex) line(ok, '/ark-* skills current for detected tools');
|
|
1174
|
-
if (legacyCodex) {
|
|
1175
|
-
line(warn, 'Codex: legacy flat .codex/prompts only (not a loadable skill catalog)');
|
|
1176
|
-
}
|
|
1177
|
-
if (codexLegacySafeDelete) {
|
|
1178
|
-
line(
|
|
1179
|
-
' ',
|
|
1180
|
-
color.dim(
|
|
1181
|
-
'Codex catalog complete — leftover .codex/prompts/ark-*.md are safe to delete (not loadable; not required).'
|
|
1182
|
-
)
|
|
1183
|
-
);
|
|
1184
|
-
}
|
|
1185
|
-
if (remMiss + remStale > 0) {
|
|
1186
|
-
line(
|
|
1187
|
-
warn,
|
|
1188
|
-
`${remMiss} missing / ${remStale} content-behind-package /ark-* skill(s) for ${remainingGaps.map((g) => g.tool).join(', ')}`
|
|
1189
|
-
);
|
|
1190
|
-
}
|
|
1191
|
-
const codexHomeGap = detectCodexHomeGap(root);
|
|
1192
|
-
if (codexHomeGap) {
|
|
1193
|
-
const parts = [
|
|
1194
|
-
codexHomeGap.legacyPromptsOnly ? 'legacy-prompts-only' : null,
|
|
1195
|
-
codexHomeGap.missing > 0 ? `${codexHomeGap.missing} missing` : null,
|
|
1196
|
-
codexHomeGap.stale > 0 ? `${codexHomeGap.stale} content-behind-package` : null, codexHomeGap.catalogStateReason,
|
|
1197
|
-
].filter(Boolean);
|
|
1198
|
-
const deferred = !codexConcernIsActive();
|
|
1199
|
-
// Deferred home debt is dim/info (not warn) so non-Codex sessions are not "incomplete".
|
|
1200
|
-
if (deferred) {
|
|
1201
|
-
line(color.dim('·'), color.dim(`Codex home skills ${parts.join(', ')} (deferred — not on Codex session)`));
|
|
1202
|
-
} else {
|
|
1203
|
-
line(warn, `Codex home skills ${parts.join(', ')}`);
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
for (const gap of agentHomeGaps) {
|
|
1207
|
-
const parts = [
|
|
1208
|
-
gap.missing > 0 ? `${gap.missing} missing` : null,
|
|
1209
|
-
gap.stale > 0 ? `${gap.stale} content-behind-package` : null,
|
|
1210
|
-
gap.catalogStateReason,
|
|
1211
|
-
].filter(Boolean);
|
|
1212
|
-
const deferred = !agentHomeConcernIsActive(gap.host);
|
|
1213
|
-
const summary = `${gap.label} shared agent skills ${parts.join(', ')}`;
|
|
1214
|
-
if (deferred) {
|
|
1215
|
-
line(color.dim('·'), color.dim(`${summary} (deferred — not this session)`));
|
|
1216
|
-
} else {
|
|
1217
|
-
line(warn, summary);
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
console.log('');
|
|
1222
|
-
console.log(color.bold('Baseline'));
|
|
1223
|
-
if (!baseline.exists) {
|
|
1224
|
-
line(!analysisComplete || violations.length > 0 ? warn : ok, !analysisComplete ? 'No baseline — current violations were not fully evaluated' : violations.length > 0 ? 'No baseline — adopting a dirty repo? freeze with --update-baseline' : 'No baseline (nothing to freeze)');
|
|
1225
|
-
} else {
|
|
1226
|
-
// Baseline keys are line-agnostic, so N keys can suppress ≥N violations — label as keys
|
|
1227
|
-
// to avoid an apparent mismatch with the "frozen" violation count above.
|
|
1228
|
-
const baseMark = !analysisComplete || baselineHonesty.dirtyBaselineRisk ? warn : ok;
|
|
1229
|
-
line(baseMark, `${baseline.keys.size} frozen key(s)${analysisComplete ? '' : ' — stale comparison not verified'}`);
|
|
1230
|
-
if (analysisComplete && baselineHonesty.dirtyBaselineRisk) {
|
|
1231
|
-
line(warn, baselineHonesty.message);
|
|
1232
|
-
}
|
|
1233
|
-
if (analysisComplete && staleBaseline > 0) {
|
|
1234
|
-
line(warn, `${staleBaseline} stale entr(y/ies) no longer occur — tighten with --update-baseline`);
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
console.log('');
|
|
1239
|
-
console.log(color.bold('Command runners'));
|
|
1240
|
-
if (staleRunners.length === 0) line(ok, 'Emitted commands match the package manager');
|
|
1241
|
-
else {
|
|
1242
|
-
line(warn, `Stale runner in ${staleRunners.join(', ')}`);
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
// Adoption completeness (hosts, MCP health, codex home, core optionality, origin, baseline policy)
|
|
1246
|
-
console.log('');
|
|
1247
|
-
console.log(color.bold('Adoption (separate from fitness score)'));
|
|
1248
|
-
if (adoption.gaps.length === 0 && !adoption.layerBalance) {
|
|
1249
|
-
line(
|
|
1250
|
-
ok,
|
|
1251
|
-
'Hosts, MCP argv, core optionality, origin report, baseline policy, and deploy-path lint/types look complete'
|
|
1252
|
-
);
|
|
1253
|
-
} else {
|
|
1254
|
-
for (const gap of adoption.gaps) {
|
|
1255
|
-
// Deferred Codex-home debt (non-temp) is annotated, not a top action, when the
|
|
1256
|
-
// session host is not Codex — fix when that host is used.
|
|
1257
|
-
const mark = gap.deferred
|
|
1258
|
-
? color.dim('·')
|
|
1259
|
-
: gap.severity === 'warn'
|
|
1260
|
-
? warn
|
|
1261
|
-
: gap.severity === 'info'
|
|
1262
|
-
? warn
|
|
1263
|
-
: bad;
|
|
1264
|
-
line(mark, gap.message);
|
|
1265
|
-
if (gap.fix) {
|
|
1266
|
-
line(' ', color.dim(gap.deferred ? `When using Codex: ${gap.fix}` : `Fix: ${gap.fix}`));
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
if (adoption.layerBalance) {
|
|
1270
|
-
line(warn, color.dim(adoption.layerBalance.educational));
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
if (adoption.baseline) {
|
|
1274
|
-
line(
|
|
1275
|
-
' ',
|
|
1276
|
-
color.dim(
|
|
1277
|
-
`Baseline policy: ${adoption.baseline.signal}` +
|
|
1278
|
-
(adoption.baseline.primaryPathUsesBaseline
|
|
1279
|
-
? ' · primary path uses --baseline'
|
|
1280
|
-
: ' · primary path does not use --baseline')
|
|
1281
|
-
)
|
|
1282
|
-
);
|
|
1283
|
-
}
|
|
1284
|
-
if (adoption.originReport.present) {
|
|
1285
|
-
line(ok, 'Origin architecture snapshot present (.ark/reports/origin.json)');
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
console.log('');
|
|
1289
|
-
console.log(color.bold('Safety / bypass resistance'));
|
|
1290
|
-
const safety = options.safety;
|
|
1291
|
-
if (!safety) {
|
|
1292
|
-
line(warn, 'Safety diagnostics unavailable');
|
|
1293
|
-
} else {
|
|
1294
|
-
const rows = [
|
|
1295
|
-
['Non-literal dynamic dependencies', safety.nonLiteralDynamicImports],
|
|
1296
|
-
['@ts-ignore / @ts-nocheck', safety.tsSuppressions],
|
|
1297
|
-
['Explicit any casts', safety.anyCasts],
|
|
1298
|
-
['InMemory stores in production source', safety.inMemoryProductionStores],
|
|
1299
|
-
['Rules with peerIsolation: false', safety.disabledPeerIsolationRules],
|
|
1300
|
-
];
|
|
1301
|
-
for (const [label, entries] of rows) {
|
|
1302
|
-
line(entries.length === 0 ? ok : warn, `${label}: ${entries.length}`);
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
|
|
933
|
+
printDoctorCompactHuman(humanView);
|
|
934
|
+
if (options.all) printDoctorDetailsHuman(humanView);
|
|
1306
935
|
}
|
|
936
|
+
|