arkgate 2.6.0 → 2.6.1
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 +61 -0
- package/README.md +90 -67
- package/bin/ark-check.mjs +264 -51
- package/bin/ark-layer-match.mjs +29 -0
- package/bin/ark-mcp.mjs +102 -5
- package/bin/ark-shared.mjs +295 -6
- package/bin/ark.mjs +44 -34
- package/bin/lib/agent-gates.mjs +448 -15
- package/bin/lib/doctor-plan.mjs +11 -4
- package/bin/lib/presets.mjs +75 -4
- package/dist/eslint/index.cjs.map +1 -1
- package/dist/eslint/index.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +1 -1
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.js +1 -1
- package/dist/nestjs/index.js.map +1 -1
- package/docs/agent-guide.md +63 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/templates/skills/ark-adopt.md +43 -87
- package/templates/skills/ark-autopilot.md +39 -77
- package/templates/skills/ark-contract.md +43 -84
- package/templates/skills/ark-coverage.md +62 -83
- package/templates/skills/ark-fix.md +45 -90
- package/templates/skills/ark-loop.md +44 -66
package/bin/ark-shared.mjs
CHANGED
|
@@ -224,6 +224,10 @@ export function applyFrameworkLayoutOverlays(config, root) {
|
|
|
224
224
|
'src/services/**',
|
|
225
225
|
'src/use-cases/**',
|
|
226
226
|
'src/lib/**',
|
|
227
|
+
// Common Next "app core" bags (API clients, hooks, auth, stores) — not UI routes.
|
|
228
|
+
// Without these, monorepos like */src/core/** stay ungoverned and produce false greens.
|
|
229
|
+
'src/core/**',
|
|
230
|
+
'**/core/**',
|
|
227
231
|
'src/actions/**',
|
|
228
232
|
'src/**/actions.ts',
|
|
229
233
|
'src/**/actions.tsx',
|
|
@@ -244,6 +248,20 @@ export function applyFrameworkLayoutOverlays(config, root) {
|
|
|
244
248
|
'src/lib/prisma/**',
|
|
245
249
|
'src/server/db/**',
|
|
246
250
|
]);
|
|
251
|
+
// Demo assets, generated public output, and tool configs are not architecture surface.
|
|
252
|
+
const nextExcludes = [
|
|
253
|
+
'**/public/**',
|
|
254
|
+
'**/*.config.js',
|
|
255
|
+
'**/*.config.ts',
|
|
256
|
+
'**/*.config.mjs',
|
|
257
|
+
'**/playwright*.ts',
|
|
258
|
+
'**/eslint.config.*',
|
|
259
|
+
'**/postcss.config.*',
|
|
260
|
+
'**/prettier.config.*',
|
|
261
|
+
'**/rstest.config.*',
|
|
262
|
+
'**/scripts/**',
|
|
263
|
+
];
|
|
264
|
+
next.exclude = [...new Set([...(next.exclude ?? []), ...nextExcludes])];
|
|
247
265
|
next.frameworkOverlay = next.frameworkOverlay
|
|
248
266
|
? `${next.frameworkOverlay}+next`
|
|
249
267
|
: 'next';
|
|
@@ -369,6 +387,9 @@ export {
|
|
|
369
387
|
layerForFile,
|
|
370
388
|
layerForRelativePath,
|
|
371
389
|
isEdgeDenied,
|
|
390
|
+
DEFAULT_GENERATED_FILE_GLOBS,
|
|
391
|
+
scanExcludePatterns,
|
|
392
|
+
isScanExcludedRelative,
|
|
372
393
|
} from './ark-layer-match.mjs';
|
|
373
394
|
|
|
374
395
|
function normalizePrefix(prefix) {
|
|
@@ -719,7 +740,15 @@ function readPackageJson(root) {
|
|
|
719
740
|
}
|
|
720
741
|
}
|
|
721
742
|
|
|
722
|
-
/**
|
|
743
|
+
/**
|
|
744
|
+
* Workspace / multi-package roots for monorepo include.
|
|
745
|
+
* Sources (universal — no project-specific names):
|
|
746
|
+
* 1. package.json workspaces + pnpm-workspace.yaml
|
|
747
|
+
* 2. rush.json projectFolder top segments (JSONC-tolerant)
|
|
748
|
+
* 3. lerna.json packages globs
|
|
749
|
+
* 4. If still empty: conventional multi-package top-level dirs that exist and
|
|
750
|
+
* contain at least one package.json (packages, apps, plugins, services, …)
|
|
751
|
+
*/
|
|
723
752
|
export function detectWorkspaces(root) {
|
|
724
753
|
const dirs = new Set();
|
|
725
754
|
const addGlob = (glob) => {
|
|
@@ -727,9 +756,16 @@ export function detectWorkspaces(root) {
|
|
|
727
756
|
const beforeStar = glob.split('*')[0].replace(/\/+$/, '');
|
|
728
757
|
if (beforeStar && beforeStar !== '.') dirs.add(normalizeRel(beforeStar));
|
|
729
758
|
};
|
|
759
|
+
const addTop = (rel) => {
|
|
760
|
+
if (typeof rel !== 'string') return;
|
|
761
|
+
const top = rel.split(/[/\\]/).filter(Boolean)[0];
|
|
762
|
+
if (top && top !== '.') dirs.add(normalizeRel(top));
|
|
763
|
+
};
|
|
764
|
+
|
|
730
765
|
const pkg = readPackageJson(root);
|
|
731
766
|
const ws = Array.isArray(pkg?.workspaces) ? pkg.workspaces : pkg?.workspaces?.packages;
|
|
732
767
|
if (Array.isArray(ws)) ws.forEach(addGlob);
|
|
768
|
+
|
|
733
769
|
const pnpmFile = path.join(root, 'pnpm-workspace.yaml');
|
|
734
770
|
if (fs.existsSync(pnpmFile)) {
|
|
735
771
|
let inPackages = false;
|
|
@@ -744,9 +780,183 @@ export function detectWorkspaces(root) {
|
|
|
744
780
|
if (item) addGlob(item[1].trim());
|
|
745
781
|
}
|
|
746
782
|
}
|
|
783
|
+
|
|
784
|
+
// Rush monorepos often have no root package.json workspaces — only rush.json.
|
|
785
|
+
const rushFile = path.join(root, 'rush.json');
|
|
786
|
+
if (fs.existsSync(rushFile)) {
|
|
787
|
+
try {
|
|
788
|
+
let text = fs.readFileSync(rushFile, 'utf8');
|
|
789
|
+
text = text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
|
790
|
+
const rush = JSON.parse(text);
|
|
791
|
+
for (const project of rush.projects || []) {
|
|
792
|
+
if (typeof project?.projectFolder === 'string') addTop(project.projectFolder);
|
|
793
|
+
}
|
|
794
|
+
} catch {
|
|
795
|
+
/* ignore malformed rush.json */
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Lerna packages globs.
|
|
800
|
+
const lernaFile = path.join(root, 'lerna.json');
|
|
801
|
+
if (fs.existsSync(lernaFile)) {
|
|
802
|
+
try {
|
|
803
|
+
const lerna = JSON.parse(fs.readFileSync(lernaFile, 'utf8'));
|
|
804
|
+
if (Array.isArray(lerna.packages)) lerna.packages.forEach(addGlob);
|
|
805
|
+
} catch {
|
|
806
|
+
/* ignore */
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Conventional multi-package roots when no explicit workspace manifest listed roots.
|
|
811
|
+
if (dirs.size === 0) {
|
|
812
|
+
const conventional = [
|
|
813
|
+
'packages',
|
|
814
|
+
'apps',
|
|
815
|
+
'plugins',
|
|
816
|
+
'services',
|
|
817
|
+
'server',
|
|
818
|
+
'servers',
|
|
819
|
+
'server-plugins',
|
|
820
|
+
'libs',
|
|
821
|
+
'lib',
|
|
822
|
+
'modules',
|
|
823
|
+
'foundations',
|
|
824
|
+
'pods',
|
|
825
|
+
'models',
|
|
826
|
+
'clients',
|
|
827
|
+
'sdks',
|
|
828
|
+
'tools',
|
|
829
|
+
'tooling',
|
|
830
|
+
'desktop',
|
|
831
|
+
'common',
|
|
832
|
+
];
|
|
833
|
+
for (const name of conventional) {
|
|
834
|
+
const abs = path.join(root, name);
|
|
835
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) continue;
|
|
836
|
+
if (dirContainsPackageJson(abs, 2)) dirs.add(name);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
747
840
|
return [...dirs];
|
|
748
841
|
}
|
|
749
842
|
|
|
843
|
+
/** True if dir (or a child within maxDepth) has a package.json — multi-package root signal. */
|
|
844
|
+
function dirContainsPackageJson(dir, maxDepth) {
|
|
845
|
+
try {
|
|
846
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) return true;
|
|
847
|
+
if (maxDepth <= 0) return false;
|
|
848
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
849
|
+
if (!entry.isDirectory()) continue;
|
|
850
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
|
851
|
+
if (dirContainsPackageJson(path.join(dir, entry.name), maxDepth - 1)) return true;
|
|
852
|
+
}
|
|
853
|
+
} catch {
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const SKIP_DIR_NAMES = new Set([
|
|
860
|
+
'node_modules',
|
|
861
|
+
'dist',
|
|
862
|
+
'build',
|
|
863
|
+
'coverage',
|
|
864
|
+
'.git',
|
|
865
|
+
'.next',
|
|
866
|
+
'.turbo',
|
|
867
|
+
'.cache',
|
|
868
|
+
'vendor',
|
|
869
|
+
'__pycache__',
|
|
870
|
+
]);
|
|
871
|
+
|
|
872
|
+
function dirHasTsSources(dir, maxDepth) {
|
|
873
|
+
try {
|
|
874
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
875
|
+
if (entry.isFile() && /\.(tsx?|jsx?|mts|cts)$/i.test(entry.name)) return true;
|
|
876
|
+
if (!entry.isDirectory()) continue;
|
|
877
|
+
if (SKIP_DIR_NAMES.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
878
|
+
if (maxDepth > 0 && dirHasTsSources(path.join(dir, entry.name), maxDepth - 1)) return true;
|
|
879
|
+
}
|
|
880
|
+
} catch {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Discover package roots that contain TypeScript/JS sources (polyglot-safe).
|
|
888
|
+
* Returns relative paths (e.g. remotion-composer, packages/ui). Caps depth/count
|
|
889
|
+
* to avoid scanning huge trees. Universal — no project-specific names.
|
|
890
|
+
*/
|
|
891
|
+
export function detectTsPackageRoots(root, options = {}) {
|
|
892
|
+
const maxDepth = options.maxDepth ?? 3;
|
|
893
|
+
const maxRoots = options.maxRoots ?? 40;
|
|
894
|
+
const found = [];
|
|
895
|
+
|
|
896
|
+
const visit = (abs, rel, depth) => {
|
|
897
|
+
if (found.length >= maxRoots || depth > maxDepth) return;
|
|
898
|
+
let entries;
|
|
899
|
+
try {
|
|
900
|
+
entries = fs.readdirSync(abs, { withFileTypes: true });
|
|
901
|
+
} catch {
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
const hasPkg = entries.some((e) => e.isFile() && e.name === 'package.json');
|
|
905
|
+
if (hasPkg && rel && dirHasTsSources(abs, 3)) {
|
|
906
|
+
found.push(rel.split(path.sep).join('/'));
|
|
907
|
+
// Do not descend into nested packages under an already-selected package root
|
|
908
|
+
// unless maxDepth allows and we want monorepo packages/* children — still scan children
|
|
909
|
+
// for nested packages (packages/foo).
|
|
910
|
+
}
|
|
911
|
+
if (depth >= maxDepth) return;
|
|
912
|
+
for (const entry of entries) {
|
|
913
|
+
if (!entry.isDirectory()) continue;
|
|
914
|
+
if (SKIP_DIR_NAMES.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
915
|
+
// Skip agent skill asset trees (not app code).
|
|
916
|
+
if (entry.name === 'skills' || entry.name === 'templates' || entry.name === 'fixtures') continue;
|
|
917
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
918
|
+
visit(path.join(abs, entry.name), childRel, depth + 1);
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
visit(root, '', 0);
|
|
923
|
+
// Also treat root itself if it has package.json + TS
|
|
924
|
+
if (fs.existsSync(path.join(root, 'package.json')) && dirHasTsSources(root, 2)) {
|
|
925
|
+
if (!found.includes('.')) {
|
|
926
|
+
// Prefer explicit '.' only when no nested packages found
|
|
927
|
+
if (found.length === 0) found.push('.');
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return [...new Set(found)].sort();
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Merge workspace globs with TS package roots. If workspaces alone would miss
|
|
935
|
+
* all TS (or are empty), fill from detectTsPackageRoots.
|
|
936
|
+
*/
|
|
937
|
+
export function resolveIncludeRoots(root) {
|
|
938
|
+
const workspaces = detectWorkspaces(root);
|
|
939
|
+
const tsRoots = detectTsPackageRoots(root);
|
|
940
|
+
if (workspaces.length === 0) {
|
|
941
|
+
if (tsRoots.length > 0) return tsRoots.filter((r) => r !== '.');
|
|
942
|
+
return [];
|
|
943
|
+
}
|
|
944
|
+
// Workspaces present: keep them, add TS package roots not covered by a workspace prefix
|
|
945
|
+
const merged = new Set(workspaces);
|
|
946
|
+
for (const tr of tsRoots) {
|
|
947
|
+
if (tr === '.') continue;
|
|
948
|
+
const covered = workspaces.some(
|
|
949
|
+
(w) => tr === w || tr.startsWith(`${w}/`) || w.startsWith(`${tr}/`)
|
|
950
|
+
);
|
|
951
|
+
if (!covered) merged.add(tr.split('/')[0]); // top segment if nested
|
|
952
|
+
// Prefer full package path when it's a direct child
|
|
953
|
+
if (!tr.includes('/') || workspaces.includes(tr.split('/')[0])) {
|
|
954
|
+
if (fs.existsSync(path.join(root, tr, 'package.json'))) merged.add(tr);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return [...merged];
|
|
958
|
+
}
|
|
959
|
+
|
|
750
960
|
function isSourceFile(name) {
|
|
751
961
|
return /\.(tsx?|jsx?|mjsx?|cjsx?|mts|cts)$/i.test(name);
|
|
752
962
|
}
|
|
@@ -822,6 +1032,47 @@ function countTsxFiles(files) {
|
|
|
822
1032
|
return files.filter((f) => /\.(tsx|jsx)$/i.test(f)).length;
|
|
823
1033
|
}
|
|
824
1034
|
|
|
1035
|
+
/**
|
|
1036
|
+
* Merge root + nested package.json dependencies (frontend/, packages/*, apps/*, …).
|
|
1037
|
+
* Hosts that only put `arkgate` at the monorepo root and Next under frontend/ must
|
|
1038
|
+
* still detect nextFramework so layout overlays (src/core/**) apply on day one.
|
|
1039
|
+
*/
|
|
1040
|
+
export function collectAggregatedDeps(root) {
|
|
1041
|
+
const deps = {};
|
|
1042
|
+
const mergePkg = (pkg) => {
|
|
1043
|
+
if (!pkg || typeof pkg !== 'object') return;
|
|
1044
|
+
for (const key of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
1045
|
+
const block = pkg[key];
|
|
1046
|
+
if (block && typeof block === 'object') Object.assign(deps, block);
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
mergePkg(readPackageJson(root));
|
|
1050
|
+
let entries = [];
|
|
1051
|
+
try {
|
|
1052
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
1053
|
+
} catch {
|
|
1054
|
+
return deps;
|
|
1055
|
+
}
|
|
1056
|
+
for (const entry of entries) {
|
|
1057
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
1058
|
+
const childRoot = path.join(root, entry.name);
|
|
1059
|
+
mergePkg(readPackageJson(childRoot));
|
|
1060
|
+
// One level under conventional multi-package roots
|
|
1061
|
+
if (['packages', 'apps', 'services', 'plugins', 'packages-internal'].includes(entry.name)) {
|
|
1062
|
+
try {
|
|
1063
|
+
for (const sub of fs.readdirSync(childRoot, { withFileTypes: true })) {
|
|
1064
|
+
if (sub.isDirectory() && !sub.name.startsWith('.')) {
|
|
1065
|
+
mergePkg(readPackageJson(path.join(childRoot, sub.name)));
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
} catch {
|
|
1069
|
+
/* ignore */
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
return deps;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
825
1076
|
/**
|
|
826
1077
|
* Collect deterministic repo shape signals for architecture archetype scoring.
|
|
827
1078
|
* Vendor packages may appear in toolHints only — never as the primary label.
|
|
@@ -830,7 +1081,20 @@ export function collectRepoShapeSignals(root) {
|
|
|
830
1081
|
const pkg = readPackageJson(root);
|
|
831
1082
|
const workspaceDirs = detectWorkspaces(root);
|
|
832
1083
|
const workspaces = workspaceDirs.length > 0;
|
|
833
|
-
|
|
1084
|
+
// Include frontend/web/client — common Next monorepo app folders (deer-flow-style).
|
|
1085
|
+
const candidateScanDirs = [
|
|
1086
|
+
'src',
|
|
1087
|
+
'lib',
|
|
1088
|
+
'api',
|
|
1089
|
+
'packages',
|
|
1090
|
+
'apps',
|
|
1091
|
+
'frontend',
|
|
1092
|
+
'web',
|
|
1093
|
+
'client',
|
|
1094
|
+
'app',
|
|
1095
|
+
...workspaceDirs,
|
|
1096
|
+
];
|
|
1097
|
+
const srcDirs = [...new Set(candidateScanDirs)].filter((d) =>
|
|
834
1098
|
fs.existsSync(path.join(root, d))
|
|
835
1099
|
);
|
|
836
1100
|
const scanRoots = srcDirs.length > 0 ? srcDirs.map((d) => path.join(root, d)) : [root];
|
|
@@ -838,13 +1102,14 @@ export function collectRepoShapeSignals(root) {
|
|
|
838
1102
|
const sourceFileCount = sourceFiles.length;
|
|
839
1103
|
const tinyTree = sourceFileCount < 3;
|
|
840
1104
|
|
|
841
|
-
|
|
1105
|
+
// Nested package.json deps (not root-only) — critical for monorepo Next under frontend/
|
|
1106
|
+
const deps = collectAggregatedDeps(root);
|
|
842
1107
|
const hasUiFramework = Object.keys(deps).some((name) =>
|
|
843
1108
|
/^(react|react-dom|vue|svelte|preact|solid-js)$/i.test(name.split('/')[0])
|
|
844
1109
|
);
|
|
845
1110
|
const srcUiFiles = sourceFiles.filter((file) => {
|
|
846
1111
|
const rel = path.relative(root, file).split(path.sep).join('/');
|
|
847
|
-
return rel.startsWith('src/') && /\.(tsx|jsx)$/i.test(file);
|
|
1112
|
+
return (rel.includes('/src/') || rel.startsWith('src/')) && /\.(tsx|jsx)$/i.test(file);
|
|
848
1113
|
});
|
|
849
1114
|
|
|
850
1115
|
const topNames = new Set(srcDirs.flatMap((d) => listTopLevelDirNames(root, d)));
|
|
@@ -860,7 +1125,8 @@ export function collectRepoShapeSignals(root) {
|
|
|
860
1125
|
const rel = path.relative(root, file).split(path.sep).join('/');
|
|
861
1126
|
return (
|
|
862
1127
|
/(^|\/)next\.config\./.test(rel) ||
|
|
863
|
-
/
|
|
1128
|
+
// app/page.tsx OR app/dashboard/page.tsx (middle segment optional)
|
|
1129
|
+
/(^|\/)app\/(?:.*\/)?page\.(t|j)sx?$/.test(rel) ||
|
|
864
1130
|
/(^|\/)pages\/.+\.(t|j)sx?$/.test(rel)
|
|
865
1131
|
);
|
|
866
1132
|
});
|
|
@@ -1127,7 +1393,15 @@ export function scoreArchetypes(signals, playbook) {
|
|
|
1127
1393
|
const rawConfidence = top.score / top.maxPositive;
|
|
1128
1394
|
const margin =
|
|
1129
1395
|
second && second.score > 0 ? (top.score - second.score) / Math.max(top.score, 1) : 0.25;
|
|
1130
|
-
|
|
1396
|
+
let confidence = Math.min(1, Math.max(0.1, rawConfidence * 0.7 + margin * 0.3));
|
|
1397
|
+
// Thin / zero TS surface: never present a high-confidence archetype as the sole answer.
|
|
1398
|
+
const thinTs =
|
|
1399
|
+
!signals.sourceFileCount ||
|
|
1400
|
+
signals.sourceFileCount < 8 ||
|
|
1401
|
+
signals.tinyTree;
|
|
1402
|
+
if (thinTs) {
|
|
1403
|
+
confidence = Math.min(confidence, 0.28);
|
|
1404
|
+
}
|
|
1131
1405
|
|
|
1132
1406
|
return {
|
|
1133
1407
|
ranked: scored,
|
|
@@ -1135,6 +1409,13 @@ export function scoreArchetypes(signals, playbook) {
|
|
|
1135
1409
|
label: top.label,
|
|
1136
1410
|
preset: top.preset,
|
|
1137
1411
|
confidence: Math.round(confidence * 1000) / 1000,
|
|
1412
|
+
...(thinTs
|
|
1413
|
+
? {
|
|
1414
|
+
thinTsSurface: true,
|
|
1415
|
+
caution:
|
|
1416
|
+
'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.',
|
|
1417
|
+
}
|
|
1418
|
+
: {}),
|
|
1138
1419
|
phases: top.phases,
|
|
1139
1420
|
analogy: top.analogy,
|
|
1140
1421
|
antiPatterns: top.antiPatterns,
|
|
@@ -1169,6 +1450,7 @@ export function buildArchitectureRecommendation(root, options = {}) {
|
|
|
1169
1450
|
label: result.label,
|
|
1170
1451
|
preset: result.preset,
|
|
1171
1452
|
confidence: result.confidence,
|
|
1453
|
+
...(result.thinTsSurface ? { thinTsSurface: true, caution: result.caution } : {}),
|
|
1172
1454
|
phases: result.phases,
|
|
1173
1455
|
adoptInOrder,
|
|
1174
1456
|
analogy: result.analogy,
|
|
@@ -1334,6 +1616,13 @@ export function formatArchitectureRecommendationHuman(recommendation) {
|
|
|
1334
1616
|
lines.push('');
|
|
1335
1617
|
lines.push(`Archetype: ${recommendation.archetype} — ${recommendation.label}`);
|
|
1336
1618
|
lines.push(`Preset: ${recommendation.preset} (confidence ${recommendation.confidence})`);
|
|
1619
|
+
if (recommendation.thinTsSurface) {
|
|
1620
|
+
lines.push('');
|
|
1621
|
+
lines.push(
|
|
1622
|
+
`⚠ Thin TypeScript surface (${recommendation.signals?.sourceFileCount ?? 0} files) — confidence is capped. Do not treat this as a firm shape.`
|
|
1623
|
+
);
|
|
1624
|
+
if (recommendation.caution) lines.push(recommendation.caution);
|
|
1625
|
+
}
|
|
1337
1626
|
if (recommendation.runnerUp?.id) {
|
|
1338
1627
|
lines.push(
|
|
1339
1628
|
`Runner-up: ${recommendation.runnerUp.id}${recommendation.runnerUp.label ? ` (${recommendation.runnerUp.label})` : ''}`
|
package/bin/ark.mjs
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
buildArchitectureRecommendation,
|
|
11
11
|
detectPackageManager,
|
|
12
12
|
detectWorkspaces,
|
|
13
|
+
resolveIncludeRoots,
|
|
14
|
+
detectTsPackageRoots,
|
|
13
15
|
INIT_WIZARD_CHOICES,
|
|
14
16
|
isValidArchetypeId,
|
|
15
17
|
mapWizardChoiceToArchetype,
|
|
@@ -345,16 +347,32 @@ async function start(args) {
|
|
|
345
347
|
if (!fs.existsSync(configPath)) {
|
|
346
348
|
const initArgs = ['--root', root, '--init'];
|
|
347
349
|
const preset = archetype ? resolveArchetypePreset(archetype).preset : undefined;
|
|
350
|
+
const includeRoots = resolveIncludeRoots(root);
|
|
351
|
+
const tsPackages = detectTsPackageRoots(root);
|
|
348
352
|
const workspaces = detectWorkspaces(root);
|
|
349
353
|
const looksLikeMonorepo =
|
|
354
|
+
includeRoots.length > 0 ||
|
|
355
|
+
tsPackages.length > 0 ||
|
|
350
356
|
workspaces.length > 0 ||
|
|
357
|
+
fs.existsSync(path.join(root, 'rush.json')) ||
|
|
358
|
+
fs.existsSync(path.join(root, 'pnpm-workspace.yaml')) ||
|
|
359
|
+
fs.existsSync(path.join(root, 'lerna.json')) ||
|
|
351
360
|
fs.existsSync(path.join(root, 'apps')) ||
|
|
352
361
|
fs.existsSync(path.join(root, 'packages'));
|
|
353
|
-
// Mature multi-package trees must NOT get a thin src/** starter (0 files
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
362
|
+
// Mature multi-package / nested-TS trees must NOT get a thin src/** starter (0 files).
|
|
363
|
+
if (looksLikeMonorepo && (rec?.mature || includeRoots.length > 0 || tsPackages.length > 0)) {
|
|
364
|
+
// UI-heavy TS packages (Remotion/Vite) prefer ui-surface patterns when recommend says so.
|
|
365
|
+
const useUi =
|
|
366
|
+
rec?.preset === 'feature-sliced' ||
|
|
367
|
+
rec?.archetype === 'frontend-surface' ||
|
|
368
|
+
(tsPackages.length > 0 && includeRoots.length === 0 && !rec?.mature);
|
|
369
|
+
initArgs.push('--preset', useUi && tsPackages.length <= 3 ? 'ui-surface' : 'monorepo');
|
|
370
|
+
const shown = includeRoots.length > 0 ? includeRoots : tsPackages;
|
|
371
|
+
console.log(
|
|
372
|
+
shown.length > 0
|
|
373
|
+
? ` Multi-package / TS package layout detected — profile include: ${shown.join(', ')}.`
|
|
374
|
+
: ' Multi-package layout detected — using monorepo profile.'
|
|
375
|
+
);
|
|
358
376
|
} else if (!rec?.mature && preset) {
|
|
359
377
|
initArgs.push('--preset', preset);
|
|
360
378
|
}
|
|
@@ -363,7 +381,12 @@ async function start(args) {
|
|
|
363
381
|
} else {
|
|
364
382
|
console.log(' Found an existing ark.config.json — keeping it.');
|
|
365
383
|
}
|
|
366
|
-
|
|
384
|
+
{
|
|
385
|
+
const gateArgs = ['--root', root, '--install-agent-gates'];
|
|
386
|
+
if (args.tools) gateArgs.push('--tools', args.tools);
|
|
387
|
+
if (args.force) gateArgs.push('--force');
|
|
388
|
+
runArkCheck(gateArgs, { cwd: root });
|
|
389
|
+
}
|
|
367
390
|
|
|
368
391
|
// 4) Show the plan: what's safe to auto-fix vs what needs a decision.
|
|
369
392
|
console.log('');
|
|
@@ -406,49 +429,36 @@ async function start(args) {
|
|
|
406
429
|
planOk = false;
|
|
407
430
|
}
|
|
408
431
|
|
|
409
|
-
// 5) Plain-language wrap-up —
|
|
410
|
-
//
|
|
411
|
-
// enforce = contract actually governs code and gates stay on.
|
|
432
|
+
// 5) Plain-language wrap-up — one next step, status light only.
|
|
433
|
+
// Modes are detected (Suggest/Adapt/Enforce), not user-picked settings.
|
|
412
434
|
console.log('');
|
|
413
435
|
if (mode === 'enforce' && planOk) {
|
|
414
|
-
console.log('Done —
|
|
436
|
+
console.log('Done — status: ENFORCE (gates can honestly protect you).');
|
|
415
437
|
console.log('What happens now:');
|
|
416
438
|
console.log(' • Every edit is checked (in CI and, if wired, at write time).');
|
|
417
|
-
console.log(' • Carry out remaining plan steps with /ark-autopilot (safe auto + your approvals).');
|
|
418
439
|
} else if (mode === 'suggest') {
|
|
419
|
-
console.log('Done —
|
|
440
|
+
console.log('Done — status: SUGGEST (starting shape installed; expand as you grow).');
|
|
420
441
|
console.log('What happens now:');
|
|
421
|
-
console.log(' • Gates are on for whatever the contract already matches.');
|
|
422
|
-
console.log(' • Expand coverage as you create real layer folders (see the plan above).');
|
|
423
442
|
if (governedPercent != null) {
|
|
424
|
-
console.log(` •
|
|
443
|
+
console.log(` • Ark governs ~${governedPercent}% of in-scope files — low is normal on a fresh scaffold.`);
|
|
425
444
|
}
|
|
426
|
-
console.log(' • When you want the agent to drive the plan: /ark-autopilot');
|
|
427
445
|
} else {
|
|
428
|
-
console.log('Done —
|
|
446
|
+
console.log('Done — status: ADAPT (contract still aligning with your real layout).');
|
|
429
447
|
console.log('What happens now:');
|
|
430
448
|
if (governedPercent != null) {
|
|
431
449
|
console.log(
|
|
432
|
-
` • Governed
|
|
450
|
+
` • Governed ~${governedPercent}% — a "clean" plan with low coverage checks almost nothing.`
|
|
433
451
|
);
|
|
434
452
|
}
|
|
435
|
-
console.log(` • See what is unmatched: ${arkCommand(root, 'ark-check', '--coverage')}`);
|
|
436
|
-
console.log(' • On a mature repo, prefer /ark-adopt over forcing a starter preset.');
|
|
437
|
-
console.log(' • Drive fixes with /ark-autopilot once the contract matches; until then ENFORCE is not honest.');
|
|
438
|
-
}
|
|
439
|
-
console.log(` • Re-run the plan anytime: ${arkCommand(root, 'ark-check', '--plan')}`);
|
|
440
|
-
console.log(` • Full project check: ${arkCommand(root, 'ark-check', '--root . --config ark.config.json --strict-config')}`);
|
|
441
|
-
console.log(` • Adoption health: ${arkCommand(root, 'ark-check', '--doctor')}`);
|
|
442
|
-
console.log(` • Update Ark later: ${arkCommand(root, 'ark', 'upgrade')}`);
|
|
443
|
-
if (fs.existsSync(path.join(root, '.ark-baseline.json'))) {
|
|
444
|
-
console.log(
|
|
445
|
-
' • Baseline file present — keep empty for ratchet-from-clean, or freeze debt with --update-baseline.'
|
|
446
|
-
);
|
|
447
|
-
} else {
|
|
448
|
-
console.log(
|
|
449
|
-
' • No baseline yet (fine on clean trees). Adopting dirty code? freeze with --update-baseline.'
|
|
450
|
-
);
|
|
451
453
|
}
|
|
454
|
+
console.log('');
|
|
455
|
+
console.log('Next (the only flow you need):');
|
|
456
|
+
console.log(' 1. In your agent: /ark-autopilot');
|
|
457
|
+
console.log(' → origin report, adoption, plan, safe fixes, leave gates on.');
|
|
458
|
+
console.log(` 2. Status anytime: ${arkCommand(root, 'ark-check', '--doctor')}`);
|
|
459
|
+
console.log(` 3. After edits: ${arkCommand(root, 'ark-check', '--root . --config ark.config.json --strict-config')}`);
|
|
460
|
+
console.log('');
|
|
461
|
+
console.log('Optional later: --plan · --coverage · /ark-fix · /ark-place · ark upgrade');
|
|
452
462
|
|
|
453
463
|
// 6) First architecture report — freezes an origin snapshot under .ark/reports/
|
|
454
464
|
// so later --report runs can show evolution. Idempotent: origin is written only once.
|