arkgate 2.8.1 → 2.8.3
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 +57 -2
- package/README.md +4 -1
- package/bin/ark-check.mjs +8 -0
- package/bin/ark-shared.mjs +67 -5
- package/bin/ark.mjs +55 -9
- package/bin/lib/agent-gates.mjs +136 -23
- package/bin/lib/config-warnings.mjs +1 -1
- package/bin/lib/core-ratchet.mjs +152 -0
- package/bin/lib/doctor-plan.mjs +16 -0
- package/bin/lib/html-report.mjs +15 -0
- package/bin/lib/presets.mjs +86 -12
- 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/dist/runtime/index.cjs +1 -1
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/index.js.map +1 -1
- package/docs/brownfield-adoption.md +1 -1
- package/docs/package-surface.md +3 -0
- package/package.json +2 -2
- package/server.json +2 -2
- package/templates/skills/ark-autopilot.md +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,61 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to ArkGate (`arkgate`; formerly `ark-runtime-kernel`) are documented here.
|
|
4
4
|
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
## 2.8.3 — 2026-07-09
|
|
8
|
+
|
|
9
|
+
Field residuals + official site: core ratchet to honest ENFORCE, typecheck bootstrap,
|
|
10
|
+
host-token scrub, arkgate.online homepage. **No intentional CLI flag or JSON shape breaks.**
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`--ratchet-cores`:** when architecture is green (0 active violations, governed ≥ 50%),
|
|
15
|
+
set `optional: false` on **populated** core layers only so doctor can report **ENFORCE**
|
|
16
|
+
honestly. Empty cores stay optional (no false-ENFORCE theatre). Doctor core-optional gaps
|
|
17
|
+
point at this command; `/ark-autopilot` documents the step after goal.met.
|
|
18
|
+
- **Typecheck bootstrap:** `ark start` / `--install-agent-gates` add `"typecheck": "tsc --noEmit"`
|
|
19
|
+
when `tsconfig.json`/`jsconfig.json` exists and no typecheck-like script is present; generated
|
|
20
|
+
CI includes the typecheck step. Existing scripts are never overwritten.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **Official website:** product homepage is [arkgate.online](https://www.arkgate.online/)
|
|
25
|
+
(`package.json` `homepage`, README badges/footer, ROADMAP identity). npm package page
|
|
26
|
+
and GitHub remain source + distribution links.
|
|
27
|
+
- **Repo hygiene:** scrub named field-probe host identities from docs, comments, tests, and
|
|
28
|
+
historical changelog wording; fixtures stay framework-generic (Nest/Next only).
|
|
29
|
+
- **Maintainability:** `--ratchet-cores` lives in `bin/lib/core-ratchet.mjs` (not the ark-check
|
|
30
|
+
entry); typecheck detection uses shared `packageScriptsHaveTypecheck` (not full deploy-path
|
|
31
|
+
scan); typecheck bootstrap is skipped under `--skills-only`.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## 2.8.2 — 2026-07-09
|
|
35
|
+
|
|
36
|
+
Field-honesty patch (Next/UI host probe): no Nest false positives, no false ENFORCE on
|
|
37
|
+
ui-surface bags, honest `ark start` mode, Next proxy/middleware classification.
|
|
38
|
+
**No intentional CLI flag or JSON shape breaks.**
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- **Nest detection false positive:** bare `*.service.ts` / similar names no longer set
|
|
43
|
+
`nestFramework` without `@nestjs/*` or controller/module/gateway/resolver files (Next/Node
|
|
44
|
+
apps no longer get a spurious `nestjs+next` overlay).
|
|
45
|
+
- **False ENFORCE on UI bags:** doctor/report mode stays **ADAPT** when Domain+Persistence are
|
|
46
|
+
empty while Presentation dominates, or when core layers with files remain `optional: true`.
|
|
47
|
+
- **ui-surface / Next defaults:** drop whole-`src` and bare `**/lib/**` presentation bags;
|
|
48
|
+
classify conventional data clients (`lib/supabase`, `lib/airtable`, `lib/prisma`, …) as
|
|
49
|
+
**Persistence**; add Application patterns for actions/services.
|
|
50
|
+
- **Generated CI:** when `package.json` has `lint` / `typecheck` scripts, the installed
|
|
51
|
+
GitHub Actions workflow runs them before ark-check (closes deploy-path gaps for Next hosts).
|
|
52
|
+
- **`ark start` wrap-up mode:** prefers `ark-check --doctor` `operatingMode` over plan-only
|
|
53
|
+
`resolveOperatingMode` (default **adapt**, double-lock against false **ENFORCE**).
|
|
54
|
+
- **Next middleware / proxy:** ui-surface + Next overlay classify `src/proxy.ts`, root
|
|
55
|
+
`proxy.ts`, and classic `middleware.ts` as **Presentation** (Next 16 rename no longer
|
|
56
|
+
leaves the edge entry ungoverned).
|
|
57
|
+
- **Idempotent Next overlay:** re-applying framework overlays no longer yields
|
|
58
|
+
`frameworkOverlay: "next+next"`.
|
|
59
|
+
|
|
5
60
|
## 2.8.1 — 2026-07-09
|
|
6
61
|
|
|
7
62
|
Runtime honesty release (roadmap **R8–R9**): EventBus publish pipeline decomposition and
|
|
@@ -84,7 +139,7 @@ shape breaks** for the gate/co-pilot path.
|
|
|
84
139
|
|
|
85
140
|
## 2.6.1 — 2026-07-09
|
|
86
141
|
|
|
87
|
-
Field-test release: Next/monorepo honesty (
|
|
142
|
+
Field-test release: Next/monorepo honesty (frontend monorepo hosts), simplified **one-flow** UX for
|
|
88
143
|
humans and autonomous agents, and skills that require real source remediation—not CLI paraphrase.
|
|
89
144
|
|
|
90
145
|
### Fixed — false greens & strict CI noise (Next / monorepo)
|
|
@@ -132,7 +187,7 @@ Templates under `templates/skills/` (and project `.grok/skills` copies) for at l
|
|
|
132
187
|
|
|
133
188
|
### Tests
|
|
134
189
|
|
|
135
|
-
- Fixture-style unit tests drive real `bin/ark-check.mjs`: Next core governance;
|
|
190
|
+
- Fixture-style unit tests drive real `bin/ark-check.mjs`: Next core governance; frontend monorepo-like
|
|
136
191
|
monorepo (`frontend` + `core/**/types.ts` not Domain); strict-config with dead globs;
|
|
137
192
|
empty baseline removal.
|
|
138
193
|
|
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
Your AI writes most of the code. ArkGate keeps that code inside an architecture you can trust —
|
|
8
8
|
and makes sure a “green” check means something real.
|
|
9
9
|
|
|
10
|
+
[](https://www.arkgate.online/)
|
|
10
11
|
[](https://github.com/pedroknigge/arkgate/actions/workflows/ci.yml)
|
|
11
12
|
[](https://www.npmjs.com/package/arkgate)
|
|
12
13
|
[](LICENSE)
|
|
@@ -267,10 +268,12 @@ npm run typecheck
|
|
|
267
268
|
npm run check:architecture # Ark gates itself
|
|
268
269
|
```
|
|
269
270
|
|
|
271
|
+
**Website:** [arkgate.online](https://www.arkgate.online/)
|
|
270
272
|
**npm:** [`arkgate`](https://www.npmjs.com/package/arkgate) · formerly `ark-runtime-kernel`
|
|
271
273
|
**Product:** **ArkGate** — architecture co-pilot / gate for AI TypeScript (not a runtime kernel).
|
|
272
274
|
CLI: `arkgate` · `arkgate-check` · `arkgate-mcp` (aliases `ark` / `ark-check` / `ark-mcp` still work for one major).
|
|
273
|
-
MCP registry: `io.github.pedroknigge/arkgate`.
|
|
275
|
+
MCP registry: `io.github.pedroknigge/arkgate`.
|
|
276
|
+
**Source:** [github.com/pedroknigge/arkgate](https://github.com/pedroknigge/arkgate)
|
|
274
277
|
|
|
275
278
|
Node ≥ 18 · **MIT**.
|
|
276
279
|
|
package/bin/ark-check.mjs
CHANGED
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
runPlan,
|
|
61
61
|
runDoctor,
|
|
62
62
|
} from './lib/doctor-plan.mjs';
|
|
63
|
+
import { runRatchetCores } from './lib/core-ratchet.mjs';
|
|
63
64
|
import {
|
|
64
65
|
baselineKey,
|
|
65
66
|
readBaseline,
|
|
@@ -153,6 +154,7 @@ function parseArgs(argv) {
|
|
|
153
154
|
else if (arg === '--apply-policy-pack') args.applyPolicyPack = argv[++i];
|
|
154
155
|
else if (arg === '--suggest-include') args.suggestInclude = true;
|
|
155
156
|
else if (arg === '--adopt-contract') args.adoptContract = true;
|
|
157
|
+
else if (arg === '--ratchet-cores') args.ratchetCores = true;
|
|
156
158
|
else if (arg === '--write') args.write = true;
|
|
157
159
|
else if (arg === '--watch') args.watch = true;
|
|
158
160
|
else if (arg === '--beginner') args.beginner = true;
|
|
@@ -203,6 +205,7 @@ function usage() {
|
|
|
203
205
|
' ark-check --apply-policy-pack <id> [--force] write ark.config.json from templates/policy-packs/ (uses preset factory)',
|
|
204
206
|
' ark-check --suggest-include [--json] propose include roots (TS packages / workspaces)',
|
|
205
207
|
' ark-check --adopt-contract [--write] expand include + UI patterns from ungoverned dirs (contract adopt)',
|
|
208
|
+
' ark-check --ratchet-cores when raw graph is green (0 violations; baseline ignored), set optional:false on populated cores only (writes ark.config.json)',
|
|
206
209
|
' ark-check --watch re-run the check when governed files change (debounced)',
|
|
207
210
|
' ark-check --report [file.html] [--beginner] [--reset-origin] [--no-archive]',
|
|
208
211
|
' HTML report + snapshots under .ark/reports/ (origin once, latest each run, history JSON)',
|
|
@@ -1046,6 +1049,11 @@ async function main() {
|
|
|
1046
1049
|
return;
|
|
1047
1050
|
}
|
|
1048
1051
|
|
|
1052
|
+
if (args.ratchetCores) {
|
|
1053
|
+
runRatchetCores(root, config, files, rules, violations, args, { displayPathFromRoot });
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1049
1057
|
if (args.updateBaseline) {
|
|
1050
1058
|
const summary = summarizeViolations(violations);
|
|
1051
1059
|
// Bloquear y avisar: a lopsided freeze buries a likely contract bug as "debt". Refuse it
|
package/bin/ark-shared.mjs
CHANGED
|
@@ -217,6 +217,15 @@ export function applyFrameworkLayoutOverlays(config, root) {
|
|
|
217
217
|
'src/**/error.tsx',
|
|
218
218
|
'src/**/route.ts',
|
|
219
219
|
'src/**/route.tsx',
|
|
220
|
+
// Next middleware edge entry (classic + Next 16 proxy rename)
|
|
221
|
+
'src/middleware.ts',
|
|
222
|
+
'src/middleware.js',
|
|
223
|
+
'middleware.ts',
|
|
224
|
+
'middleware.js',
|
|
225
|
+
'src/proxy.ts',
|
|
226
|
+
'src/proxy.js',
|
|
227
|
+
'proxy.ts',
|
|
228
|
+
'proxy.js',
|
|
220
229
|
]);
|
|
221
230
|
mergeLayerPatterns(next, 'ApplicationOrchestration', [
|
|
222
231
|
'src/features/**',
|
|
@@ -247,6 +256,21 @@ export function applyFrameworkLayoutOverlays(config, root) {
|
|
|
247
256
|
'src/lib/db/**',
|
|
248
257
|
'src/lib/prisma/**',
|
|
249
258
|
'src/server/db/**',
|
|
259
|
+
// Conventional client data bags under lib/ (higher specificity than bare src/lib/**).
|
|
260
|
+
'src/lib/supabase/**',
|
|
261
|
+
'src/lib/airtable/**',
|
|
262
|
+
'src/lib/firebase/**',
|
|
263
|
+
'src/lib/firestore/**',
|
|
264
|
+
'src/lib/mongodb/**',
|
|
265
|
+
'src/lib/mongoose/**',
|
|
266
|
+
'src/lib/drizzle/**',
|
|
267
|
+
'src/lib/kysely/**',
|
|
268
|
+
'src/lib/planetscale/**',
|
|
269
|
+
'src/lib/neon/**',
|
|
270
|
+
'**/lib/supabase/**',
|
|
271
|
+
'**/lib/airtable/**',
|
|
272
|
+
'**/lib/prisma/**',
|
|
273
|
+
'**/lib/db/**',
|
|
250
274
|
]);
|
|
251
275
|
// Demo assets, generated public output, and tool configs are not architecture surface.
|
|
252
276
|
const nextExcludes = [
|
|
@@ -262,9 +286,12 @@ export function applyFrameworkLayoutOverlays(config, root) {
|
|
|
262
286
|
'**/scripts/**',
|
|
263
287
|
];
|
|
264
288
|
next.exclude = [...new Set([...(next.exclude ?? []), ...nextExcludes])];
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
289
|
+
// Idempotent: re-applying overlays (e.g. re-init / re-start) must not yield "next+next".
|
|
290
|
+
if (!String(next.frameworkOverlay || '').split('+').includes('next')) {
|
|
291
|
+
next.frameworkOverlay = next.frameworkOverlay
|
|
292
|
+
? `${next.frameworkOverlay}+next`
|
|
293
|
+
: 'next';
|
|
294
|
+
}
|
|
268
295
|
}
|
|
269
296
|
|
|
270
297
|
if (signals.expressLike && !signals.nestFramework) {
|
|
@@ -323,15 +350,47 @@ export function applyFrameworkLayoutOverlays(config, root) {
|
|
|
323
350
|
/**
|
|
324
351
|
* Operating mode for the co-pilot surfaces (not "who the user is"):
|
|
325
352
|
* suggest | adapt | enforce
|
|
353
|
+
*
|
|
354
|
+
* ENFORCE means gates can honestly protect the tree. High governed% alone is not
|
|
355
|
+
* enough when core layers are empty (presentation bag false-green) or core layers
|
|
356
|
+
* with real files remain optional: true.
|
|
357
|
+
*
|
|
358
|
+
* @param {object} opts
|
|
359
|
+
* @param {number|null} [opts.governedPercent]
|
|
360
|
+
* @param {boolean|null} [opts.planMet]
|
|
361
|
+
* @param {boolean} [opts.mature]
|
|
362
|
+
* @param {number|null} [opts.totalFiles]
|
|
363
|
+
* @param {string[]} [opts.emptyLayers] layer names with zero matched files
|
|
364
|
+
* @param {number} [opts.coreOptionalWithFiles] count of core layers that have files but optional:true
|
|
365
|
+
* @param {number|null} [opts.presentationShare] PresentationAdapters file share 0..1 when known
|
|
326
366
|
*/
|
|
327
367
|
export function resolveOperatingMode({
|
|
328
368
|
governedPercent = null,
|
|
329
369
|
planMet = null,
|
|
330
370
|
mature = false,
|
|
331
371
|
totalFiles = null,
|
|
372
|
+
emptyLayers = null,
|
|
373
|
+
coreOptionalWithFiles = 0,
|
|
374
|
+
presentationShare = null,
|
|
332
375
|
} = {}) {
|
|
333
376
|
// Zero files in scope is never ENFORCE — the contract is not looking at any code.
|
|
334
377
|
if (totalFiles === 0) return 'adapt';
|
|
378
|
+
// Core layers still optional while holding real files → contract weaker than the tree.
|
|
379
|
+
if (coreOptionalWithFiles > 0) return 'adapt';
|
|
380
|
+
// Presentation-bag false green: almost everything is Presentation, Domain+Persistence empty.
|
|
381
|
+
const empty = Array.isArray(emptyLayers) ? emptyLayers : [];
|
|
382
|
+
const domainEmpty = empty.includes('DomainModel');
|
|
383
|
+
const persistenceEmpty = empty.includes('PersistenceAdapters');
|
|
384
|
+
if (
|
|
385
|
+
planMet === true &&
|
|
386
|
+
domainEmpty &&
|
|
387
|
+
persistenceEmpty &&
|
|
388
|
+
presentationShare != null &&
|
|
389
|
+
presentationShare >= 0.5 &&
|
|
390
|
+
(governedPercent ?? 0) >= 50
|
|
391
|
+
) {
|
|
392
|
+
return 'adapt';
|
|
393
|
+
}
|
|
335
394
|
if (planMet === true && (governedPercent == null || governedPercent >= 50)) return 'enforce';
|
|
336
395
|
if (governedPercent != null && governedPercent < 50) return 'adapt';
|
|
337
396
|
if (mature) return 'adapt';
|
|
@@ -994,7 +1053,7 @@ export function collectRepoShapeSignals(root) {
|
|
|
994
1053
|
const pkg = readPackageJson(root);
|
|
995
1054
|
const workspaceDirs = detectWorkspaces(root);
|
|
996
1055
|
const workspaces = workspaceDirs.length > 0;
|
|
997
|
-
// Include frontend/web/client — common Next monorepo app folders
|
|
1056
|
+
// Include frontend/web/client — common Next monorepo app folders.
|
|
998
1057
|
const candidateScanDirs = [
|
|
999
1058
|
'src',
|
|
1000
1059
|
'lib',
|
|
@@ -1027,10 +1086,13 @@ export function collectRepoShapeSignals(root) {
|
|
|
1027
1086
|
|
|
1028
1087
|
const topNames = new Set(srcDirs.flatMap((d) => listTopLevelDirNames(root, d)));
|
|
1029
1088
|
// Framework / filename signals — strong enough that a tiny Nest starter is not a "prototype".
|
|
1089
|
+
// Nest detection: require real Nest surface (@nestjs/* or controller/module/gateway/resolver).
|
|
1090
|
+
// Do NOT treat bare `*.service.ts` / `*.guard.ts` as Nest — many Next/Node apps use those
|
|
1091
|
+
// names without Nest (false nestjs overlay + wrong doctor toolHints).
|
|
1030
1092
|
const nestFramework =
|
|
1031
1093
|
Object.keys(deps).some((name) => name.startsWith('@nestjs/')) ||
|
|
1032
1094
|
sourceFiles.some((file) =>
|
|
1033
|
-
/\.(controller|module|
|
|
1095
|
+
/\.(controller|module|gateway|resolver)\.ts$/i.test(file)
|
|
1034
1096
|
);
|
|
1035
1097
|
const nextFramework =
|
|
1036
1098
|
Boolean(deps.next) ||
|
package/bin/ark.mjs
CHANGED
|
@@ -393,27 +393,64 @@ async function start(args) {
|
|
|
393
393
|
console.log('Your architecture plan:');
|
|
394
394
|
runArkCheck(['--root', root, '--config', 'ark.config.json', '--plan'], { cwd: root });
|
|
395
395
|
|
|
396
|
-
// Capture plan JSON for an honest wrap-up
|
|
396
|
+
// Capture plan + doctor JSON for an honest wrap-up. Mode MUST match --doctor
|
|
397
|
+
// (emptyLayers, core-optional, presentation bag) — never claim ENFORCE from plan alone.
|
|
397
398
|
const planCapture = spawnSync(
|
|
398
399
|
process.execPath,
|
|
399
400
|
[arkCheck, '--root', root, '--config', 'ark.config.json', '--plan', '--json'],
|
|
400
401
|
{ cwd: root, encoding: 'utf8' }
|
|
401
402
|
);
|
|
403
|
+
const doctorCapture = spawnSync(
|
|
404
|
+
process.execPath,
|
|
405
|
+
[arkCheck, '--root', root, '--config', 'ark.config.json', '--doctor', '--json'],
|
|
406
|
+
{ cwd: root, encoding: 'utf8' }
|
|
407
|
+
);
|
|
402
408
|
let planOk = true;
|
|
403
409
|
let governedPercent = null;
|
|
404
|
-
let mode = '
|
|
410
|
+
let mode = 'adapt'; // conservative default — never default to enforce
|
|
405
411
|
try {
|
|
406
412
|
const parsed = JSON.parse(planCapture.stdout || '{}');
|
|
407
413
|
planOk = parsed.ok === true && parsed.plan?.goal?.met === true;
|
|
408
414
|
governedPercent = parsed.plan?.goal?.governedPercent ?? null;
|
|
409
415
|
const totalFiles = parsed.plan?.goal?.totalFiles ?? null;
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
416
|
+
|
|
417
|
+
let doctorMode = null;
|
|
418
|
+
try {
|
|
419
|
+
const doc = JSON.parse(doctorCapture.stdout || '{}');
|
|
420
|
+
doctorMode = doc.doctor?.operatingMode ?? null;
|
|
421
|
+
// Prefer doctor's mode (includes emptyLayers + coreOptional + presentation-bag honesty).
|
|
422
|
+
if (doctorMode === 'suggest' || doctorMode === 'adapt' || doctorMode === 'enforce') {
|
|
423
|
+
mode = doctorMode;
|
|
424
|
+
} else {
|
|
425
|
+
// Fallback: recompute with honesty inputs from doctor payload when present.
|
|
426
|
+
const emptyLayers = doc.doctor?.emptyLayers ?? [];
|
|
427
|
+
const coreOptionalWithFiles = Array.isArray(doc.doctor?.adoption?.coreOptional)
|
|
428
|
+
? doc.doctor.adoption.coreOptional.length
|
|
429
|
+
: 0;
|
|
430
|
+
const total = totalFiles || doc.doctor?.governed?.totalFiles || 0;
|
|
431
|
+
const presentationShare =
|
|
432
|
+
total > 0 && typeof doc.doctor?.governed?.percent === 'number'
|
|
433
|
+
? null // presentationShare only from coverage layers when available
|
|
434
|
+
: null;
|
|
435
|
+
mode = resolveOperatingMode({
|
|
436
|
+
governedPercent: totalFiles === 0 ? 0 : governedPercent,
|
|
437
|
+
planMet: parsed.plan?.goal?.met === true,
|
|
438
|
+
mature: Boolean(rec?.mature),
|
|
439
|
+
totalFiles: totalFiles ?? total,
|
|
440
|
+
emptyLayers,
|
|
441
|
+
coreOptionalWithFiles,
|
|
442
|
+
presentationShare,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
} catch {
|
|
446
|
+
mode = resolveOperatingMode({
|
|
447
|
+
governedPercent: totalFiles === 0 ? 0 : governedPercent,
|
|
448
|
+
planMet: parsed.plan?.goal?.met === true,
|
|
449
|
+
mature: Boolean(rec?.mature),
|
|
450
|
+
totalFiles,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
417
454
|
// Fresh greenfield with good coverage but no real tree yet → suggest, not enforce theatre.
|
|
418
455
|
if (mode === 'enforce' && rec && !rec.mature && (governedPercent ?? 0) < 80) {
|
|
419
456
|
mode = 'suggest';
|
|
@@ -423,6 +460,10 @@ async function start(args) {
|
|
|
423
460
|
mode = 'adapt';
|
|
424
461
|
planOk = false;
|
|
425
462
|
}
|
|
463
|
+
// Never claim ENFORCE wrap-up if doctor would not (double-lock).
|
|
464
|
+
if (mode === 'enforce' && doctorMode && doctorMode !== 'enforce') {
|
|
465
|
+
mode = doctorMode;
|
|
466
|
+
}
|
|
426
467
|
} catch {
|
|
427
468
|
// If capture fails, stay conservative: don't claim full enforcement.
|
|
428
469
|
mode = 'adapt';
|
|
@@ -457,6 +498,11 @@ async function start(args) {
|
|
|
457
498
|
console.log(' → origin report, adoption, plan, safe fixes, leave gates on.');
|
|
458
499
|
console.log(` 2. Status anytime: ${arkCommand(root, 'ark-check', '--doctor')}`);
|
|
459
500
|
console.log(` 3. After edits: ${arkCommand(root, 'ark-check', '--root . --config ark.config.json --strict-config')}`);
|
|
501
|
+
if (mode === 'adapt' && planOk) {
|
|
502
|
+
console.log(
|
|
503
|
+
` 4. When green but cores still optional: ${arkCommand(root, 'ark-check', '--ratchet-cores')} → honest ENFORCE`
|
|
504
|
+
);
|
|
505
|
+
}
|
|
460
506
|
console.log('');
|
|
461
507
|
console.log('Optional later: --plan · --coverage · /ark-fix · /ark-place · ark upgrade');
|
|
462
508
|
|
package/bin/lib/agent-gates.mjs
CHANGED
|
@@ -20,8 +20,9 @@ import {
|
|
|
20
20
|
DEFAULT_DOMAIN_FORBIDDEN_GLOBALS,
|
|
21
21
|
DEFAULT_RULES,
|
|
22
22
|
createElevenLayerConfig,
|
|
23
|
-
applyFrameworkLayoutOverlays
|
|
23
|
+
applyFrameworkLayoutOverlays,
|
|
24
24
|
} from '../ark-shared.mjs';
|
|
25
|
+
import { CORE_LAYER_NAMES } from './core-ratchet.mjs';
|
|
25
26
|
import {
|
|
26
27
|
assessCodexHomeMcp,
|
|
27
28
|
codexArkBlockHasPreferredBin,
|
|
@@ -76,6 +77,97 @@ export function hasCheckArchitectureScript(root) {
|
|
|
76
77
|
return Boolean(pkg?.scripts?.['check:architecture']);
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Whether package.json scripts already expose a typecheck-like command.
|
|
82
|
+
* Shared by deploy-path quality + typecheck bootstrap (single definition).
|
|
83
|
+
* @param {Record<string, unknown>|null|undefined} scripts
|
|
84
|
+
*/
|
|
85
|
+
export function packageScriptsHaveTypecheck(scripts) {
|
|
86
|
+
if (!scripts || typeof scripts !== 'object') return false;
|
|
87
|
+
return Boolean(
|
|
88
|
+
(typeof scripts.typecheck === 'string' && scripts.typecheck.trim()) ||
|
|
89
|
+
(typeof scripts['type-check'] === 'string' && scripts['type-check'].trim()) ||
|
|
90
|
+
(typeof scripts['check:types'] === 'string' && scripts['check:types'].trim()) ||
|
|
91
|
+
(typeof scripts.tsc === 'string' && /\btsc\b/.test(scripts.tsc))
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Root package (and shallow nested packages) already have a typecheck script.
|
|
97
|
+
* Does not scan CI or framework configs — only package.json scripts.
|
|
98
|
+
* @param {string} root
|
|
99
|
+
*/
|
|
100
|
+
export function treeHasTypecheckScript(root) {
|
|
101
|
+
const pkg = readPackageJson(root);
|
|
102
|
+
if (packageScriptsHaveTypecheck(pkg?.scripts)) return true;
|
|
103
|
+
try {
|
|
104
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
105
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
106
|
+
const candidates = [path.join(root, entry.name)];
|
|
107
|
+
try {
|
|
108
|
+
for (const child of fs.readdirSync(path.join(root, entry.name), { withFileTypes: true })) {
|
|
109
|
+
if (child.isDirectory() && !child.name.startsWith('.')) {
|
|
110
|
+
candidates.push(path.join(root, entry.name, child.name));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
/* ignore */
|
|
115
|
+
}
|
|
116
|
+
for (const dir of candidates) {
|
|
117
|
+
const pj = path.join(dir, 'package.json');
|
|
118
|
+
if (!fs.existsSync(pj)) continue;
|
|
119
|
+
try {
|
|
120
|
+
const nested = JSON.parse(fs.readFileSync(pj, 'utf8'));
|
|
121
|
+
if (packageScriptsHaveTypecheck(nested.scripts)) return true;
|
|
122
|
+
} catch {
|
|
123
|
+
/* ignore */
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
/* ignore */
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Add a conservative `typecheck` script when the host has a TS/JS project config
|
|
135
|
+
* but no typecheck-like script yet. Never overwrites an existing script.
|
|
136
|
+
*
|
|
137
|
+
* @param {string} root
|
|
138
|
+
* @param {{ write?: boolean }} [opts]
|
|
139
|
+
* @returns {{
|
|
140
|
+
* changed: boolean,
|
|
141
|
+
* reason: 'added' | 'already' | 'no-tsconfig' | 'no-package-json',
|
|
142
|
+
* script?: string,
|
|
143
|
+
* }}
|
|
144
|
+
*/
|
|
145
|
+
export function ensureTypecheckScript(root, opts = {}) {
|
|
146
|
+
const write = opts.write !== false;
|
|
147
|
+
const hasTsconfig =
|
|
148
|
+
fs.existsSync(path.join(root, 'tsconfig.json')) ||
|
|
149
|
+
fs.existsSync(path.join(root, 'jsconfig.json'));
|
|
150
|
+
if (!hasTsconfig) return { changed: false, reason: 'no-tsconfig' };
|
|
151
|
+
|
|
152
|
+
const pkgPath = path.join(root, 'package.json');
|
|
153
|
+
if (!fs.existsSync(pkgPath)) return { changed: false, reason: 'no-package-json' };
|
|
154
|
+
|
|
155
|
+
if (treeHasTypecheckScript(root)) {
|
|
156
|
+
return { changed: false, reason: 'already' };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const pkg = readPackageJson(root) || {};
|
|
160
|
+
const scripts =
|
|
161
|
+
pkg.scripts && typeof pkg.scripts === 'object' ? { ...pkg.scripts } : {};
|
|
162
|
+
const script = 'tsc --noEmit';
|
|
163
|
+
scripts.typecheck = script;
|
|
164
|
+
if (write) {
|
|
165
|
+
const next = { ...pkg, scripts };
|
|
166
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(next, null, 2)}\n`);
|
|
167
|
+
}
|
|
168
|
+
return { changed: true, reason: 'added', script };
|
|
169
|
+
}
|
|
170
|
+
|
|
79
171
|
export const REQUIRED_GATE_FILES = [
|
|
80
172
|
'AGENTS.md',
|
|
81
173
|
'.mcp.json',
|
|
@@ -483,7 +575,12 @@ export function detectCiNode(root) {
|
|
|
483
575
|
return { kind: 'default', value: DEFAULT_CI_NODE_VERSION };
|
|
484
576
|
}
|
|
485
577
|
|
|
486
|
-
|
|
578
|
+
/**
|
|
579
|
+
* @param {{ name: string, install: string, run: string, cache: string, setup: string[] }} pm
|
|
580
|
+
* @param {{ kind: string, value: string }} ciNode
|
|
581
|
+
* @param {{ hasLintScript?: boolean, hasTypecheckScript?: boolean }} [quality]
|
|
582
|
+
*/
|
|
583
|
+
export function githubWorkflow(pm, ciNode, quality = {}) {
|
|
487
584
|
// pnpm/yarn setup (corepack enable) MUST run before actions/setup-node so the package
|
|
488
585
|
// manager is on PATH when setup-node's `cache: pnpm|yarn` tries to resolve the store —
|
|
489
586
|
// otherwise the cache step fails on a fresh runner ("Unable to locate executable file: pnpm").
|
|
@@ -500,6 +597,24 @@ export function githubWorkflow(pm, ciNode) {
|
|
|
500
597
|
# of sync, your local package manager is newer than this Node's — add a
|
|
501
598
|
# .nvmrc with your Node version so CI matches the dev environment.
|
|
502
599
|
node-version: '${ciNode.value}'`;
|
|
600
|
+
// When package.json already has lint/typecheck, emit CI steps so deploy-path
|
|
601
|
+
// honesty matches local scripts (Next/CRA often run these in production build).
|
|
602
|
+
const install = pm.install || '';
|
|
603
|
+
const runPrefix = install.startsWith('pnpm')
|
|
604
|
+
? 'pnpm run'
|
|
605
|
+
: install.startsWith('yarn')
|
|
606
|
+
? 'yarn'
|
|
607
|
+
: install.startsWith('bun')
|
|
608
|
+
? 'bun run'
|
|
609
|
+
: 'npm run';
|
|
610
|
+
const qualityBlock = [
|
|
611
|
+
quality.hasTypecheckScript
|
|
612
|
+
? ` - name: Typecheck\n run: ${runPrefix} typecheck`
|
|
613
|
+
: '',
|
|
614
|
+
quality.hasLintScript ? ` - name: Lint\n run: ${runPrefix} lint` : '',
|
|
615
|
+
]
|
|
616
|
+
.filter(Boolean)
|
|
617
|
+
.join('\n');
|
|
503
618
|
return `name: Ark architecture gate
|
|
504
619
|
|
|
505
620
|
on:
|
|
@@ -520,7 +635,7 @@ ${nodeSetup}
|
|
|
520
635
|
cache: ${pm.cache}
|
|
521
636
|
- name: Install dependencies
|
|
522
637
|
run: ${pm.install}
|
|
523
|
-
- name: Ark architecture check
|
|
638
|
+
${qualityBlock ? `${qualityBlock}\n` : ''} - name: Ark architecture check
|
|
524
639
|
run: ${pm.run}
|
|
525
640
|
`;
|
|
526
641
|
}
|
|
@@ -922,14 +1037,6 @@ export function brokenMcpGateFiles(root) {
|
|
|
922
1037
|
return bad;
|
|
923
1038
|
}
|
|
924
1039
|
|
|
925
|
-
/** Core layers whose optionality matters once they match files (presets share these names). */
|
|
926
|
-
const CORE_LAYER_NAMES = new Set([
|
|
927
|
-
'DomainModel',
|
|
928
|
-
'ApplicationOrchestration',
|
|
929
|
-
'PresentationAdapters',
|
|
930
|
-
'PersistenceAdapters',
|
|
931
|
-
]);
|
|
932
|
-
|
|
933
1040
|
/**
|
|
934
1041
|
* Production deploy path quality (universal — any consumer repo).
|
|
935
1042
|
* Detects when the production build host runs ESLint / typecheck as part of
|
|
@@ -982,17 +1089,9 @@ export function detectDeployPathQuality(root) {
|
|
|
982
1089
|
(typeof s['lint:ci'] === 'string' && s['lint:ci'].trim()) ||
|
|
983
1090
|
(typeof s['check:lint'] === 'string' && s['check:lint'].trim()))
|
|
984
1091
|
);
|
|
985
|
-
const scriptHasTypecheck = (s) =>
|
|
986
|
-
Boolean(
|
|
987
|
-
s &&
|
|
988
|
-
((typeof s.typecheck === 'string' && s.typecheck.trim()) ||
|
|
989
|
-
(typeof s['type-check'] === 'string' && s['type-check'].trim()) ||
|
|
990
|
-
(typeof s['check:types'] === 'string' && s['check:types'].trim()) ||
|
|
991
|
-
(typeof s.tsc === 'string' && /\btsc\b/.test(s.tsc)))
|
|
992
|
-
);
|
|
993
1092
|
|
|
994
1093
|
let hasLintScript = scriptHasLint(scripts);
|
|
995
|
-
let hasTypecheckScript =
|
|
1094
|
+
let hasTypecheckScript = packageScriptsHaveTypecheck(scripts);
|
|
996
1095
|
const packageLintScripts = [];
|
|
997
1096
|
// Monorepo: package-level scripts count (apps/web, packages/ui, …).
|
|
998
1097
|
try {
|
|
@@ -1019,7 +1118,7 @@ export function detectDeployPathQuality(root) {
|
|
|
1019
1118
|
hasLintScript = true;
|
|
1020
1119
|
packageLintScripts.push(path.relative(root, dir).split(path.sep).join('/'));
|
|
1021
1120
|
}
|
|
1022
|
-
if (
|
|
1121
|
+
if (packageScriptsHaveTypecheck(ns)) hasTypecheckScript = true;
|
|
1023
1122
|
const nd = {
|
|
1024
1123
|
...(nested.dependencies || {}),
|
|
1025
1124
|
...(nested.devDependencies || {}),
|
|
@@ -1266,7 +1365,7 @@ export function collectAdoptionGaps(root, config, coverage) {
|
|
|
1266
1365
|
id: `core-optional-${layer.name}`,
|
|
1267
1366
|
severity: 'info',
|
|
1268
1367
|
message: `Core layer ${layer.name} has ${files} file(s) but is still optional: true — contract is weaker than the tree`,
|
|
1269
|
-
fix:
|
|
1368
|
+
fix: `${arkCommand(root, 'ark-check', '--ratchet-cores')} (when architecture is green: 0 active violations)`,
|
|
1270
1369
|
});
|
|
1271
1370
|
}
|
|
1272
1371
|
}
|
|
@@ -1602,13 +1701,27 @@ export function runInstallAgentGates(args) {
|
|
|
1602
1701
|
// overwrite (they track the package). The gate/instruction files (AGENTS.md,
|
|
1603
1702
|
// settings.json, CI workflow, rules) are the ones users customize, so a plain
|
|
1604
1703
|
// `--force` clobbers them — this is the safe way to pick up new skill versions.
|
|
1704
|
+
// Do not mutate package.json under --skills-only (typecheck bootstrap is gates/CI).
|
|
1605
1705
|
if (!args.skillsOnly) {
|
|
1706
|
+
// Bootstrap typecheck before CI template so generated workflow includes the step.
|
|
1707
|
+
const typecheckBootstrap = ensureTypecheckScript(root, { write: true });
|
|
1708
|
+
if (typecheckBootstrap.changed && !args.json) {
|
|
1709
|
+
console.log(
|
|
1710
|
+
`Added package.json script "typecheck": "${typecheckBootstrap.script}" (tsconfig present; local/CI parity).`
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1606
1713
|
// Base gates: tool-agnostic contract + CI backstop, always written.
|
|
1607
1714
|
templates.push(['AGENTS.md', agentInstructions(root)]);
|
|
1608
1715
|
templates.push(['.mcp.json', mcpJson(root)]);
|
|
1609
1716
|
templates.push([
|
|
1610
1717
|
'.github/workflows/ark-check.yml',
|
|
1611
|
-
|
|
1718
|
+
(() => {
|
|
1719
|
+
const deploy = detectDeployPathQuality(root);
|
|
1720
|
+
return githubWorkflow(pm, detectCiNode(root), {
|
|
1721
|
+
hasLintScript: deploy.hasLintScript,
|
|
1722
|
+
hasTypecheckScript: deploy.hasTypecheckScript,
|
|
1723
|
+
});
|
|
1724
|
+
})(),
|
|
1612
1725
|
]);
|
|
1613
1726
|
if (tools.has('cursor')) {
|
|
1614
1727
|
templates.push(['.cursor/mcp.json', mcpJson(root)]);
|
|
@@ -127,7 +127,7 @@ export function collectConfigWarnings(root, config, files, rules, manifest) {
|
|
|
127
127
|
// Advisory only under --strict-config: monorepo/Next presets ship many optional-looking
|
|
128
128
|
// globs (e.g. src/layouts/**, app/**) that never match when include is ["frontend"].
|
|
129
129
|
// Failing the release gate on dead preset globs caused false CI red while architecture
|
|
130
|
-
// edges were clean
|
|
130
|
+
// edges were clean on multi-package hosts. Real safety is import violations +
|
|
131
131
|
// CONFIG_UNCLASSIFIED_FILES / invalid patterns.
|
|
132
132
|
warnings.push(
|
|
133
133
|
configWarning(
|