arkgate 4.8.22 → 4.8.24
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 +91 -0
- package/README.md +7 -5
- package/bin/ark-check-runtime.mjs +27 -78
- package/bin/ark-dashboard.mjs +56 -53
- package/bin/ark-layer-match.mjs +1117 -38
- package/bin/ark-mcp-runtime.mjs +412 -138
- package/bin/ark-mcp.mjs +68 -4
- package/bin/ark-shared.mjs +39 -0
- package/bin/ark.mjs +65 -97
- package/bin/lib/adapter-contract-types.mjs +5 -0
- package/bin/lib/adapter-contract.mjs +15 -0
- package/bin/lib/agent-projection-command.mjs +5 -1
- package/bin/lib/analysis-engine.mjs +15 -11
- package/bin/lib/architecture-scan.mjs +50 -6
- package/bin/lib/ark-order-doctor.mjs +1 -1
- package/bin/lib/ark-order-facts.mjs +410 -15
- package/bin/lib/ark-order-invariants.mjs +51 -9
- package/bin/lib/ark-order-sensors.mjs +28 -101
- package/bin/lib/ark-order-types.mjs +2 -0
- package/bin/lib/ark-run-doctor.mjs +14 -0
- package/bin/lib/ark-run-facts.mjs +434 -11
- package/bin/lib/ark-run-sensors.mjs +5 -3
- package/bin/lib/arkgate-pins.mjs +91 -0
- package/bin/lib/arkrules-sensors.mjs +50 -249
- package/bin/lib/check-args.mjs +23 -1
- package/bin/lib/ci-and-commands.mjs +16 -6
- package/bin/lib/class-source-scan.mjs +479 -0
- package/bin/lib/codex-home.mjs +4 -2
- package/bin/lib/config-contract-slices.mjs +701 -0
- package/bin/lib/config-contract.mjs +5 -2
- package/bin/lib/config-warnings.mjs +32 -0
- package/bin/lib/dashboard-cli.mjs +62 -0
- package/bin/lib/design-delta.mjs +1 -1
- package/bin/lib/diagnostic-catalog.mjs +45 -6
- package/bin/lib/doctor-advisories.mjs +84 -1
- package/bin/lib/doctor-next-actions.mjs +17 -2
- package/bin/lib/doctor-plan.mjs +7 -9
- package/bin/lib/effective-contract-load.mjs +121 -59
- package/bin/lib/enforcement-honesty.mjs +22 -0
- package/bin/lib/enforcement-profiles.mjs +40 -4
- package/bin/lib/first-run-help.mjs +3 -2
- package/bin/lib/flat-parent-importers.mjs +137 -0
- package/bin/lib/flat-parent-pilot.mjs +192 -0
- package/bin/lib/gate-files.mjs +29 -2
- package/bin/lib/git-change-scope.mjs +109 -0
- package/bin/lib/github-enforcement.mjs +1 -1
- package/bin/lib/hook-templates.mjs +18 -16
- package/bin/lib/host-support-matrix.mjs +3 -1
- package/bin/lib/html-report-advisories.mjs +2 -0
- package/bin/lib/html-report-depth.mjs +3 -1
- package/bin/lib/import-resolve.mjs +13 -1
- package/bin/lib/install-migrate.mjs +50 -73
- package/bin/lib/invariant-coverage.mjs +59 -11
- package/bin/lib/mcp-adoption.mjs +9 -1
- package/bin/lib/mcp-hook-payload.mjs +401 -61
- package/bin/lib/package-manager.mjs +59 -1
- package/bin/lib/passthrough-commands.mjs +83 -0
- package/bin/lib/persistence-write-hint.mjs +81 -0
- package/bin/lib/physical-cohesion.mjs +16 -18
- package/bin/lib/pilot-loop.mjs +21 -8
- package/bin/lib/pin-dependent-gates.mjs +117 -0
- package/bin/lib/policy-delta-io.mjs +168 -10
- package/bin/lib/prepare-change.mjs +120 -6
- package/bin/lib/prepare-write.mjs +1 -0
- package/bin/lib/presets.mjs +3 -5
- package/bin/lib/remediation.mjs +23 -4
- package/bin/lib/resolved-candidate-facts.mjs +315 -55
- package/bin/lib/rules-inventory.mjs +22 -3
- package/bin/lib/rules-under-contract.mjs +179 -5
- package/bin/lib/scan-files.mjs +32 -0
- package/bin/lib/snippet-analysis.mjs +140 -6
- package/bin/lib/source-policy.mjs +34 -0
- package/bin/lib/stable-hash.mjs +90 -0
- package/bin/lib/start-install-recovery.mjs +71 -1
- package/bin/lib/start-preview.mjs +30 -0
- package/bin/lib/status-command.mjs +33 -4
- package/bin/lib/status-manifest.mjs +32 -4
- package/bin/lib/team-parliament-io.mjs +79 -75
- package/bin/lib/team-parliament.mjs +6 -1
- package/bin/lib/write-path-capabilities.mjs +4 -2
- package/dist/chunk-A5EIY3HI.cjs +5 -0
- package/dist/chunk-MBXCFLO4.js +16 -0
- package/dist/chunk-RNQHRDCQ.cjs +16 -0
- package/dist/chunk-SVSQVGFJ.js +5 -0
- package/dist/{configTypes-CTELo87-.d.ts → configTypes-BJbaeoj4.d.cts} +53 -4
- package/dist/configTypes-BJbaeoj4.d.ts +246 -0
- package/dist/eslint/index.cjs +17 -4
- package/dist/eslint/index.d.cts +359 -0
- package/dist/eslint/index.d.ts +178 -158
- package/dist/eslint/index.js +10 -4
- package/dist/index.cjs +38 -37
- package/dist/index.d.cts +5475 -0
- package/dist/index.d.ts +3408 -296
- package/dist/index.js +38 -37
- package/dist/inspectorListen-6DQVJ4DC.js +1 -0
- package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
- package/dist/nestjs/index.cjs +1 -18
- package/dist/nestjs/index.d.cts +22 -0
- package/dist/nestjs/index.d.ts +1 -3
- package/dist/nestjs/index.js +1 -18
- package/dist/order/index.cjs +1 -1
- package/dist/order/index.d.cts +155 -0
- package/dist/order/index.d.ts +15 -3
- package/dist/order/index.js +1 -1
- package/dist/runtime/index.cjs +13 -30
- package/dist/{diagnosticCatalog-WD2TPSwZ.d.ts → runtime/index.d.cts} +814 -290
- package/dist/runtime/index.d.ts +2678 -108
- package/dist/runtime/index.js +13 -30
- package/dist/{types-yDNEQ603.d.ts → types-DRinG-vT.d.cts} +576 -140
- package/dist/types-DRinG-vT.d.ts +1792 -0
- package/docs/README.md +3 -1
- package/docs/agent-guide.md +92 -15
- package/docs/ai-gates.md +160 -26
- package/docs/arkorder.md +32 -15
- package/docs/brownfield-adoption.md +5 -1
- package/docs/configuration.md +273 -12
- package/docs/develop.md +2 -1
- package/docs/diagnostics.md +122 -13
- package/docs/package-surface.md +31 -8
- package/docs/product-voice.md +1 -1
- package/package.json +59 -18
- package/schemas/ark.analysis-result.schema.json +15 -0
- package/schemas/ark.config.schema.json +163 -4
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
- package/schemas/ark.status-manifest.schema.json +22 -0
- package/server.json +3 -9
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +4 -1
- package/templates/agent-skills/ark-order/SKILL.md +4 -2
- package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
- package/templates/arkrules/PersistenceAdapters.json +1 -1
- package/templates/arkrules/PresentationAdapters.json +1 -1
- package/templates/skills/ark-adopt.md +4 -1
- package/templates/skills/ark-order.md +4 -2
- package/templates/skills/ark-runtime.md +6 -1
- package/dist/types-fQ9-KrBj.d.ts +0 -183
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* Pure CLI helper (bin/lib/arkrules-sensors.mjs). Zero Node I/O.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { findClassDeclarations, scanClassMembers } from './class-source-scan.mjs';
|
|
12
|
+
import { sourceHasPersistenceWrite, sourceImportsPersistenceDriverText } from './persistence-write-hint.mjs';
|
|
11
13
|
/** Keep in lockstep with arkRulesTypes.ARK_RULE_TIER2_SENSOR_IDS (self-contained for CLI gen). */
|
|
12
14
|
const ARK_RULE_TIER2_SENSOR_IDS = ['no-anemic-model'];
|
|
13
15
|
/**
|
|
@@ -30,7 +32,6 @@ export const DOMAIN_EVENTS_PUSH_RE = new RegExp(`\\bthis\\.${EVENTS_ARRAY_PROP}\
|
|
|
30
32
|
const EVENTS_ARRAY_RESET_RE = new RegExp(`^this\\.${EVENTS_ARRAY_PROP}\\s*=\\s*\\[\\s*\\]`);
|
|
31
33
|
const ANY_THIS_EMPTY_ARRAY_RE = /^this\.[A-Za-z_][A-Za-z0-9_]*\s*=\s*\[\s*\]/;
|
|
32
34
|
const THIS_FIELD_ASSIGNMENT_RE = /\bthis\.[A-Za-z_][A-Za-z0-9_]*\s*=(?!=)/g;
|
|
33
|
-
const SHAPE_TRUNCATED_UNTIL = 'truncatedUntil';
|
|
34
35
|
export function expectedDomainInvariantWordsPhrase() {
|
|
35
36
|
return `${DOMAIN_INVARIANT_WORDS.join(', ')}, or events-array .push(`;
|
|
36
37
|
}
|
|
@@ -59,18 +60,10 @@ function methodAssignsThis(methodName, methodBody) {
|
|
|
59
60
|
return false;
|
|
60
61
|
}
|
|
61
62
|
function attachShapeTruncation(shape, truncatedUntil) {
|
|
62
|
-
|
|
63
|
-
return shape;
|
|
64
|
-
Object.defineProperty(shape, SHAPE_TRUNCATED_UNTIL, {
|
|
65
|
-
value: truncatedUntil,
|
|
66
|
-
enumerable: false,
|
|
67
|
-
configurable: true,
|
|
68
|
-
});
|
|
69
|
-
return shape;
|
|
63
|
+
return truncatedUntil == null ? shape : { ...shape, truncatedUntil };
|
|
70
64
|
}
|
|
71
65
|
function shapeTruncatedUntil(shape) {
|
|
72
|
-
|
|
73
|
-
return typeof value === 'number' ? value : undefined;
|
|
66
|
+
return typeof shape.truncatedUntil === 'number' ? shape.truncatedUntil : undefined;
|
|
74
67
|
}
|
|
75
68
|
function shapeTruncationSuffix(shape) {
|
|
76
69
|
const until = shapeTruncatedUntil(shape);
|
|
@@ -188,9 +181,22 @@ function shapesForRule(rule, shapes, layerForFile) {
|
|
|
188
181
|
return true;
|
|
189
182
|
});
|
|
190
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* A shape the scanner could not walk to its end (unbalanced header/body) is
|
|
186
|
+
* reported, never passed in silence: later members may be invisible.
|
|
187
|
+
*/
|
|
188
|
+
function truncatedShapeViolation(rule, shape, what) {
|
|
189
|
+
const until = shapeTruncatedUntil(shape);
|
|
190
|
+
if (until == null)
|
|
191
|
+
return undefined;
|
|
192
|
+
return baseViolation(rule, shape.file, `Exported class ${shape.className} shape analysed until character ${until}; later ${what} may be invisible (sensor ${rule.sensor}).`);
|
|
193
|
+
}
|
|
191
194
|
function evaluateAggregatePrivateState(rule, shapes, layerForFile) {
|
|
192
195
|
const out = [];
|
|
193
196
|
for (const shape of shapesForRule(rule, shapes, layerForFile)) {
|
|
197
|
+
const truncated = truncatedShapeViolation(rule, shape, 'members');
|
|
198
|
+
if (truncated)
|
|
199
|
+
out.push(truncated);
|
|
194
200
|
// P1-L — require real mutability (setters or non-readonly public fields).
|
|
195
201
|
// Readonly public props on intentional value/entity shapes are false-positive-prone.
|
|
196
202
|
if (shape.hasPublicSetters || shape.hasPublicMutableFields) {
|
|
@@ -202,6 +208,9 @@ function evaluateAggregatePrivateState(rule, shapes, layerForFile) {
|
|
|
202
208
|
function evaluateAlwaysValidFactory(rule, shapes, layerForFile) {
|
|
203
209
|
const out = [];
|
|
204
210
|
for (const shape of shapesForRule(rule, shapes, layerForFile)) {
|
|
211
|
+
const truncated = truncatedShapeViolation(rule, shape, 'constructors and factories');
|
|
212
|
+
if (truncated)
|
|
213
|
+
out.push(truncated);
|
|
205
214
|
if (shape.hasPublicConstructor && !shape.hasStaticFactory) {
|
|
206
215
|
// P1-L — prefer false negatives on intentional DDD / DI aggregates: a public
|
|
207
216
|
// constructor alone is weak evidence. Only fire when mutable public surface
|
|
@@ -221,9 +230,9 @@ function evaluateDomainEventOnMutation(rule, shapes, layerForFile) {
|
|
|
221
230
|
const expected = expectedDomainInvariantWordsPhrase();
|
|
222
231
|
for (const shape of shapesForRule(rule, shapes, layerForFile)) {
|
|
223
232
|
const truncation = shapeTruncationSuffix(shape);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
233
|
+
const truncated = truncatedShapeViolation(rule, shape, 'methods');
|
|
234
|
+
if (truncated)
|
|
235
|
+
out.push(truncated);
|
|
227
236
|
for (const method of shape.mutatingMethods) {
|
|
228
237
|
if (!method.referencesGuardOrPublish) {
|
|
229
238
|
out.push(baseViolation(rule, shape.file, `Mutating method ${shape.className}.${method.name} does not reference ${expected} (sensor domain-event-on-mutation).${truncation}`));
|
|
@@ -459,24 +468,11 @@ export function collectEmptyInvariantCatalogFindings(input) {
|
|
|
459
468
|
},
|
|
460
469
|
];
|
|
461
470
|
}
|
|
462
|
-
/** IO / ORM import evidence. postgres and drizzle-orm include package subpaths. Keep in lockstep with arkOrderFacts. */
|
|
463
|
-
const IO_IMPORT_HINT_RE = /\bfrom\s+['"](?:@?prisma\/client|@supabase\/|drizzle-orm(?:\/[^'"]+)?|postgres(?:\/[^'"]+)?|typeorm|knex|mongodb|pg|mysql2|mongoose|better-sqlite3|ioredis|redis|kysely|sequelize)['"]|require\(\s*['"](?:@?prisma\/client|pg|postgres(?:\/[^'"]+)?|drizzle-orm(?:\/[^'"]+)?|knex|typeorm|mongoose)/;
|
|
464
|
-
/**
|
|
465
|
-
* Path-alias / local db module (`@/lib/db`) without resolving tsconfig.
|
|
466
|
-
* Keep in lockstep with arkOrderFacts.
|
|
467
|
-
*/
|
|
468
|
-
const IO_ALIAS_IMPORT_RE = /\bfrom\s+['"](?:@\/|~\/)?(?:[\w.-]+\/)*(?:db|database|prisma|drizzle)(?:\.[cm]?[jt]sx?)?['"]|require\(\s*['"](?:@\/|~\/)?(?:[\w.-]+\/)*(?:db|database|prisma|drizzle)/;
|
|
469
|
-
/**
|
|
470
|
-
* Write tokens that skip the aggregate when paired with a persistence driver import.
|
|
471
|
-
* Callee must be db|tx|client|prisma|drizzle (PrismaClient included); not repo.update(.
|
|
472
|
-
* Keep in lockstep with arkOrderFacts.
|
|
473
|
-
*/
|
|
474
|
-
const PERSISTENCE_WRITE_HINT_RE = /\b(?:db|tx|client|prisma(?:Client)?|drizzle)\b(?:\s*\.\s*[A-Za-z_]\w*)*\s*\.\s*(?:insert(?:One|Many)?|update(?:One|Many)?|upsert|delete(?:One|Many)?|createMany|create|replaceOne|findOneAnd(?:Update|Delete|Replace))\s*\(|\bINSERT\s+INTO\b|\bUPDATE\s+[A-Za-z_][\w.]*\s+SET\b|\bDELETE\s+FROM\b/i;
|
|
475
471
|
export function isPersistenceDriverLayer(layer) {
|
|
476
472
|
return layer === 'PersistenceAdapters';
|
|
477
473
|
}
|
|
478
474
|
export function sourceImportsPersistenceDriver(content, resolvedImports) {
|
|
479
|
-
if (
|
|
475
|
+
if (sourceImportsPersistenceDriverText(content))
|
|
480
476
|
return true;
|
|
481
477
|
if (!resolvedImports)
|
|
482
478
|
return false;
|
|
@@ -487,7 +483,7 @@ export function sourceImportsPersistenceDriver(content, resolvedImports) {
|
|
|
487
483
|
if (!specifier)
|
|
488
484
|
continue;
|
|
489
485
|
const synthetic = `from '${specifier}'`;
|
|
490
|
-
if (
|
|
486
|
+
if (sourceImportsPersistenceDriverText(synthetic))
|
|
491
487
|
return true;
|
|
492
488
|
}
|
|
493
489
|
return false;
|
|
@@ -506,7 +502,7 @@ export function deriveArkRuleFileHints(_file, content, resolvedImports) {
|
|
|
506
502
|
if (!content)
|
|
507
503
|
return null;
|
|
508
504
|
const hasIo = sourceImportsPersistenceDriver(content, resolvedImports);
|
|
509
|
-
const persistenceWrite = hasIo &&
|
|
505
|
+
const persistenceWrite = hasIo && sourceHasPersistenceWrite(content);
|
|
510
506
|
// Orchestration/adapter heuristics need a longer window; writes still fire on short probes.
|
|
511
507
|
if (content.length < 40) {
|
|
512
508
|
return persistenceWrite ? { persistenceWrite: true } : null;
|
|
@@ -549,213 +545,18 @@ export function buildArkRuleFileHints(fileContents, resolvedImportsByFile) {
|
|
|
549
545
|
}
|
|
550
546
|
return out;
|
|
551
547
|
}
|
|
552
|
-
const MEMBER_MODIFIERS = new Set([
|
|
553
|
-
'public',
|
|
554
|
-
'private',
|
|
555
|
-
'protected',
|
|
556
|
-
'static',
|
|
557
|
-
'async',
|
|
558
|
-
'readonly',
|
|
559
|
-
'abstract',
|
|
560
|
-
'override',
|
|
561
|
-
'declare',
|
|
562
|
-
'get',
|
|
563
|
-
'set',
|
|
564
|
-
]);
|
|
565
548
|
const CONTROL_FLOW_METHOD_NAMES = new Set(['if', 'match', 'when']);
|
|
566
|
-
function skipStringOrComment(src, index) {
|
|
567
|
-
const ch = src[index];
|
|
568
|
-
if (ch === '/' && src[index + 1] === '/') {
|
|
569
|
-
const nl = src.indexOf('\n', index);
|
|
570
|
-
return nl === -1 ? src.length : nl;
|
|
571
|
-
}
|
|
572
|
-
if (ch === '/' && src[index + 1] === '*') {
|
|
573
|
-
const end = src.indexOf('*/', index + 2);
|
|
574
|
-
return end === -1 ? src.length : end + 2;
|
|
575
|
-
}
|
|
576
|
-
if (ch === "'" || ch === '"' || ch === '`') {
|
|
577
|
-
let j = index + 1;
|
|
578
|
-
while (j < src.length) {
|
|
579
|
-
if (src[j] === '\\') {
|
|
580
|
-
j += 2;
|
|
581
|
-
continue;
|
|
582
|
-
}
|
|
583
|
-
if (src[j] === ch)
|
|
584
|
-
return j + 1;
|
|
585
|
-
j += 1;
|
|
586
|
-
}
|
|
587
|
-
return src.length;
|
|
588
|
-
}
|
|
589
|
-
return index;
|
|
590
|
-
}
|
|
591
|
-
function skipWsAndComments(src, index) {
|
|
592
|
-
let i = index;
|
|
593
|
-
while (i < src.length) {
|
|
594
|
-
if (/\s/.test(src[i])) {
|
|
595
|
-
i += 1;
|
|
596
|
-
continue;
|
|
597
|
-
}
|
|
598
|
-
if (src[i] === '/' && (src[i + 1] === '/' || src[i + 1] === '*')) {
|
|
599
|
-
i = skipStringOrComment(src, i);
|
|
600
|
-
continue;
|
|
601
|
-
}
|
|
602
|
-
break;
|
|
603
|
-
}
|
|
604
|
-
return i;
|
|
605
|
-
}
|
|
606
|
-
function readIdent(src, index) {
|
|
607
|
-
const ch = src[index];
|
|
608
|
-
if (!ch || !/[A-Za-z_]/.test(ch))
|
|
609
|
-
return null;
|
|
610
|
-
let j = index + 1;
|
|
611
|
-
while (j < src.length && /[A-Za-z0-9_]/.test(src[j]))
|
|
612
|
-
j += 1;
|
|
613
|
-
return { ident: src.slice(index, j), end: j };
|
|
614
|
-
}
|
|
615
|
-
function skipBalanced(src, openIndex, openCh, closeCh) {
|
|
616
|
-
if (src[openIndex] !== openCh)
|
|
617
|
-
return null;
|
|
618
|
-
let depth = 1;
|
|
619
|
-
let i = openIndex + 1;
|
|
620
|
-
while (i < src.length && depth > 0) {
|
|
621
|
-
const skipped = skipStringOrComment(src, i);
|
|
622
|
-
if (skipped !== i) {
|
|
623
|
-
i = skipped;
|
|
624
|
-
continue;
|
|
625
|
-
}
|
|
626
|
-
const ch = src[i];
|
|
627
|
-
if (ch === openCh)
|
|
628
|
-
depth += 1;
|
|
629
|
-
else if (ch === closeCh)
|
|
630
|
-
depth -= 1;
|
|
631
|
-
i += 1;
|
|
632
|
-
}
|
|
633
|
-
return depth === 0 ? i : null;
|
|
634
|
-
}
|
|
635
|
-
function scanClassMembers(body) {
|
|
636
|
-
const members = [];
|
|
637
|
-
let i = 0;
|
|
638
|
-
let truncatedAt;
|
|
639
|
-
while (i < body.length) {
|
|
640
|
-
i = skipWsAndComments(body, i);
|
|
641
|
-
if (i >= body.length)
|
|
642
|
-
break;
|
|
643
|
-
if (body[i] === ';') {
|
|
644
|
-
i += 1;
|
|
645
|
-
continue;
|
|
646
|
-
}
|
|
647
|
-
const modifiers = [];
|
|
648
|
-
let cursor = i;
|
|
649
|
-
while (true) {
|
|
650
|
-
const tok = readIdent(body, cursor);
|
|
651
|
-
if (!tok || !MEMBER_MODIFIERS.has(tok.ident))
|
|
652
|
-
break;
|
|
653
|
-
modifiers.push(tok.ident);
|
|
654
|
-
cursor = skipWsAndComments(body, tok.end);
|
|
655
|
-
}
|
|
656
|
-
const nameTok = readIdent(body, cursor);
|
|
657
|
-
if (!nameTok) {
|
|
658
|
-
i += 1;
|
|
659
|
-
continue;
|
|
660
|
-
}
|
|
661
|
-
cursor = skipWsAndComments(body, nameTok.end);
|
|
662
|
-
if (body[cursor] === '<') {
|
|
663
|
-
const afterGeneric = skipBalanced(body, cursor, '<', '>');
|
|
664
|
-
if (afterGeneric == null) {
|
|
665
|
-
truncatedAt = body.length;
|
|
666
|
-
break;
|
|
667
|
-
}
|
|
668
|
-
cursor = skipWsAndComments(body, afterGeneric);
|
|
669
|
-
}
|
|
670
|
-
if (body[cursor] === '(') {
|
|
671
|
-
const afterParen = skipBalanced(body, cursor, '(', ')');
|
|
672
|
-
if (afterParen == null) {
|
|
673
|
-
truncatedAt = body.length;
|
|
674
|
-
break;
|
|
675
|
-
}
|
|
676
|
-
cursor = skipWsAndComments(body, afterParen);
|
|
677
|
-
if (body[cursor] === ':') {
|
|
678
|
-
cursor += 1;
|
|
679
|
-
while (cursor < body.length && body[cursor] !== '{' && body[cursor] !== ';') {
|
|
680
|
-
const skipped = skipStringOrComment(body, cursor);
|
|
681
|
-
if (skipped !== cursor) {
|
|
682
|
-
cursor = skipped;
|
|
683
|
-
continue;
|
|
684
|
-
}
|
|
685
|
-
cursor += 1;
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
if (body[cursor] === '{') {
|
|
689
|
-
const afterBrace = skipBalanced(body, cursor, '{', '}');
|
|
690
|
-
if (afterBrace == null) {
|
|
691
|
-
truncatedAt = body.length;
|
|
692
|
-
break;
|
|
693
|
-
}
|
|
694
|
-
members.push({
|
|
695
|
-
name: nameTok.ident,
|
|
696
|
-
modifiers,
|
|
697
|
-
kind: 'method',
|
|
698
|
-
body: body.slice(cursor + 1, afterBrace - 1),
|
|
699
|
-
});
|
|
700
|
-
i = afterBrace;
|
|
701
|
-
continue;
|
|
702
|
-
}
|
|
703
|
-
if (body[cursor] === ';') {
|
|
704
|
-
i = cursor + 1;
|
|
705
|
-
continue;
|
|
706
|
-
}
|
|
707
|
-
i = cursor + 1;
|
|
708
|
-
continue;
|
|
709
|
-
}
|
|
710
|
-
let depthBrace = 0;
|
|
711
|
-
let depthParen = 0;
|
|
712
|
-
let depthBracket = 0;
|
|
713
|
-
while (cursor < body.length) {
|
|
714
|
-
const skipped = skipStringOrComment(body, cursor);
|
|
715
|
-
if (skipped !== cursor) {
|
|
716
|
-
cursor = skipped;
|
|
717
|
-
continue;
|
|
718
|
-
}
|
|
719
|
-
const ch = body[cursor];
|
|
720
|
-
if (ch === '{')
|
|
721
|
-
depthBrace += 1;
|
|
722
|
-
else if (ch === '}') {
|
|
723
|
-
if (depthBrace === 0)
|
|
724
|
-
break;
|
|
725
|
-
depthBrace -= 1;
|
|
726
|
-
}
|
|
727
|
-
else if (ch === '(')
|
|
728
|
-
depthParen += 1;
|
|
729
|
-
else if (ch === ')')
|
|
730
|
-
depthParen -= 1;
|
|
731
|
-
else if (ch === '[')
|
|
732
|
-
depthBracket += 1;
|
|
733
|
-
else if (ch === ']')
|
|
734
|
-
depthBracket -= 1;
|
|
735
|
-
else if (ch === ';' &&
|
|
736
|
-
depthBrace === 0 &&
|
|
737
|
-
depthParen === 0 &&
|
|
738
|
-
depthBracket === 0) {
|
|
739
|
-
cursor += 1;
|
|
740
|
-
break;
|
|
741
|
-
}
|
|
742
|
-
cursor += 1;
|
|
743
|
-
}
|
|
744
|
-
members.push({
|
|
745
|
-
name: nameTok.ident,
|
|
746
|
-
modifiers,
|
|
747
|
-
kind: 'field',
|
|
748
|
-
body: '',
|
|
749
|
-
});
|
|
750
|
-
i = cursor;
|
|
751
|
-
}
|
|
752
|
-
return { members, truncatedAt };
|
|
753
|
-
}
|
|
754
549
|
/**
|
|
755
550
|
* Lightweight class-shape extraction from TypeScript source text (no compiler).
|
|
756
551
|
* Conservative: prefers false negatives over false positives for mutability.
|
|
757
552
|
* Tooling may replace with TypeScript-API facts; sensors consume the same shape.
|
|
758
553
|
*
|
|
554
|
+
* The header and body are walked by the shared tokenizer (classSourceScan):
|
|
555
|
+
* type parameters (`class Box<T>`), generic `extends` / `implements` clauses, and
|
|
556
|
+
* braces inside strings, comments, or template literals are handled. A class whose
|
|
557
|
+
* header or body cannot be walked is still reported, with a truncation mark, so
|
|
558
|
+
* enforced sensors say so instead of passing in silence.
|
|
559
|
+
*
|
|
759
560
|
* Limitation (AR05/AR06): only `export class` / `export abstract class` forms.
|
|
760
561
|
* `export default class`, re-exported classes, and non-exported aggregates are
|
|
761
562
|
* invisible — enforced structure sensors stay silent (false negative). Silence
|
|
@@ -763,25 +564,25 @@ function scanClassMembers(body) {
|
|
|
763
564
|
*/
|
|
764
565
|
export function extractClassShapesFromSource(file, content) {
|
|
765
566
|
const shapes = [];
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
i += 1;
|
|
567
|
+
for (const decl of findClassDeclarations(content, { exportedOnly: true })) {
|
|
568
|
+
const className = decl.name;
|
|
569
|
+
if (decl.bodyStart == null) {
|
|
570
|
+
// Header seen, body not derivable: an unshaped class, never a silent pass.
|
|
571
|
+
shapes.push(attachShapeTruncation({
|
|
572
|
+
file,
|
|
573
|
+
className,
|
|
574
|
+
exported: true,
|
|
575
|
+
hasPublicMutableFields: false,
|
|
576
|
+
hasPublicSetters: false,
|
|
577
|
+
hasPublicConstructor: false,
|
|
578
|
+
hasStaticFactory: false,
|
|
579
|
+
mutatingMethods: [],
|
|
580
|
+
}, decl.start));
|
|
581
|
+
continue;
|
|
782
582
|
}
|
|
783
|
-
const
|
|
784
|
-
const classUnclosed =
|
|
583
|
+
const start = decl.bodyStart;
|
|
584
|
+
const classUnclosed = decl.bodyEnd == null;
|
|
585
|
+
const body = content.slice(start, decl.bodyEnd ?? content.length);
|
|
785
586
|
const scanned = scanClassMembers(body);
|
|
786
587
|
const truncatedUntil = classUnclosed
|
|
787
588
|
? content.length
|
package/bin/lib/check-args.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ark-check CLI flag parsing.
|
|
3
3
|
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { discoverLocalBaseRef, normalizePolicyBaseRef } from './policy-delta-io.mjs';
|
|
6
7
|
|
|
@@ -23,7 +24,24 @@ export const LOCAL_STRICT_MERGE_MESSAGE =
|
|
|
23
24
|
'--local cannot be combined with --strict-merge: CI stays the full fail-closed check. Use --local (or ARK_CHECK_LOCAL=1) only on the local / pre-push path.';
|
|
24
25
|
|
|
25
26
|
export const LOCAL_TREE_MODE_MESSAGE =
|
|
26
|
-
'--local cannot be combined with report modes that need the whole tree (--doctor, --coverage, --plan, --report, --promote). Use --local --base <ref> for the cheap local check.';
|
|
27
|
+
'--local cannot be combined with report modes that need the whole tree (--doctor, --coverage, --plan, --report, --promote) or with --update-baseline. Use --local --base <ref> for the cheap local check.';
|
|
28
|
+
|
|
29
|
+
export const UPDATE_BASELINE_SCOPE_MESSAGE =
|
|
30
|
+
'--update-baseline cannot be combined with a changed-files scope (--changed, --local, or a --persona that implies --changed): the baseline is a whole-tree record, and a changed-files scan would overwrite it with only that subset and delete every other frozen key. Drop the scope flag (ARK_CHECK_LOCAL=1 is ignored for a baseline update) and re-run.';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Baseline file that suppresses frozen findings for this run, or null.
|
|
34
|
+
* Explicit `--baseline` always wins. `--report` (without `--against`) applies the committed
|
|
35
|
+
* `.ark-baseline.json` by default, the same file `--doctor` reads, so report, `ark status`
|
|
36
|
+
* and doctor agree on what is active vs frozen.
|
|
37
|
+
*/
|
|
38
|
+
export function effectiveBaselineName(args, root, exists = fs.existsSync) {
|
|
39
|
+
if (args?.baseline) return args.baseline;
|
|
40
|
+
// A merge verdict (--strict / --strict-merge / --contract-diff) never inherits the
|
|
41
|
+
// committed freeze implicitly: adding --report must not flip a fail-closed run to pass.
|
|
42
|
+
if (!args?.report || args.against || args.strictMerge || args.contractDiff) return null;
|
|
43
|
+
return exists(path.join(root, '.ark-baseline.json')) ? '.ark-baseline.json' : null;
|
|
44
|
+
}
|
|
27
45
|
|
|
28
46
|
export function localCheckEnvelope(args, root) {
|
|
29
47
|
if (!args?.local) return {};
|
|
@@ -48,6 +66,7 @@ export function applyLocalCheckMode(args, env = process.env) {
|
|
|
48
66
|
args.plan && '--plan',
|
|
49
67
|
args.report && '--report',
|
|
50
68
|
args.promote && '--promote',
|
|
69
|
+
args.updateBaseline && '--update-baseline',
|
|
51
70
|
].filter(Boolean);
|
|
52
71
|
if (args.strictMerge) {
|
|
53
72
|
if (explicit) throw new Error(LOCAL_STRICT_MERGE_MESSAGE);
|
|
@@ -288,5 +307,8 @@ export function parseArgs(argv, env = process.env) {
|
|
|
288
307
|
);
|
|
289
308
|
}
|
|
290
309
|
}
|
|
310
|
+
if (args.updateBaseline && (args.changed || args.local)) {
|
|
311
|
+
throw new Error(UPDATE_BASELINE_SCOPE_MESSAGE);
|
|
312
|
+
}
|
|
291
313
|
return applyLocalCheckMode(args, env);
|
|
292
314
|
}
|
|
@@ -21,6 +21,7 @@ import { renderHostSupportMatrixMarkdown } from './host-support-matrix.mjs';
|
|
|
21
21
|
import { PREFERRED_MCP_BIN } from './hook-templates.mjs';
|
|
22
22
|
import { hasCheckArchitectureScript, readPackageJson } from './gate-files.mjs';
|
|
23
23
|
import { arkPackageVersion } from './skill-install.mjs';
|
|
24
|
+
import { arkgateIsProjectDependency, pinnedArkgateRunner } from './package-manager.mjs';
|
|
24
25
|
|
|
25
26
|
// Field-install helpers re-exported for callers that import from this module.
|
|
26
27
|
export {
|
|
@@ -43,6 +44,17 @@ export function checkArgsForRoot(root, { requireGates = false } = {}) {
|
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
export function packageManager(root) {
|
|
47
|
+
const pm = localPackageManager(root);
|
|
48
|
+
if (arkgateIsProjectDependency(root)) return pm;
|
|
49
|
+
// arkgate is not pinned: the local runner would ask npm for a package named `ark-check`
|
|
50
|
+
// (404) or run a stale global, so name the exact arkgate that generated this workflow.
|
|
51
|
+
const run = `${pinnedArkgateRunner()} ark-check ${checkArgsForRoot(root, { requireGates: true })}`;
|
|
52
|
+
// No package.json: nothing to install, and setup-node's cache needs a lockfile.
|
|
53
|
+
if (!readPackageJson(root)) return { cache: null, setup: [], install: null, run };
|
|
54
|
+
return { ...pm, run };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function localPackageManager(root) {
|
|
46
58
|
// CI always require-gates; baseline follows checkArgsForRoot.
|
|
47
59
|
const checkArgs = checkArgsForRoot(root, { requireGates: true });
|
|
48
60
|
// Same detection as every emitted command (execRunner): honors the packageManager field and
|
|
@@ -243,10 +255,11 @@ ${rows}`;
|
|
|
243
255
|
* Load project layers for AGENTS generation. Returns null when config is absent/invalid
|
|
244
256
|
* so callers fall back to the stock 11-layer table.
|
|
245
257
|
* @param {string} root
|
|
258
|
+
* @param {string} [configPath] contract file (defaults to `<root>/ark.config.json`)
|
|
246
259
|
*/
|
|
247
|
-
export function loadConfigLayersForAgents(root) {
|
|
260
|
+
export function loadConfigLayersForAgents(root, configPath = path.join(root, 'ark.config.json')) {
|
|
248
261
|
try {
|
|
249
|
-
const cfgPath =
|
|
262
|
+
const cfgPath = configPath;
|
|
250
263
|
if (!fs.existsSync(cfgPath)) return null;
|
|
251
264
|
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
252
265
|
if (!Array.isArray(cfg?.layers) || cfg.layers.length === 0) return null;
|
|
@@ -638,10 +651,7 @@ ${setupSteps ? `${setupSteps}\n` : ''} - name: Setup Node
|
|
|
638
651
|
uses: actions/setup-node@v4
|
|
639
652
|
with:
|
|
640
653
|
${nodeSetup}
|
|
641
|
-
cache: ${pm.cache}
|
|
642
|
-
- name: Install dependencies
|
|
643
|
-
run: ${pm.install}
|
|
644
|
-
${qualityBlock ? `${qualityBlock}\n` : ''} - name: Ark architecture check
|
|
654
|
+
${pm.cache ? ` cache: ${pm.cache}\n` : ''}${pm.install ? ` - name: Install dependencies\n run: ${pm.install}\n` : ''}${qualityBlock ? `${qualityBlock}\n` : ''} - name: Ark architecture check
|
|
645
655
|
env:
|
|
646
656
|
ARK_POLICY_BASE_REF: \${{ github.event.pull_request.base.sha || github.event.before }}
|
|
647
657
|
run: |
|