arkgate 4.8.23 → 4.8.25
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 +153 -0
- package/README.md +7 -5
- package/bin/ark-check-runtime.mjs +41 -91
- package/bin/ark-dashboard.mjs +56 -53
- package/bin/ark-layer-match.mjs +707 -121
- package/bin/ark-mcp-runtime.mjs +448 -169
- package/bin/ark-mcp.mjs +68 -4
- package/bin/ark-shared.mjs +42 -22
- 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/adoption-stance.mjs +2 -2
- package/bin/lib/adr-path.mjs +3 -3
- package/bin/lib/agent-gates.mjs +0 -61
- package/bin/lib/agent-homes.mjs +12 -21
- package/bin/lib/agent-projection-command.mjs +9 -5
- package/bin/lib/ambient-state.mjs +9 -30
- package/bin/lib/analysis-engine.mjs +15 -11
- package/bin/lib/architecture-scan.mjs +61 -15
- package/bin/lib/ark-order-doctor.mjs +4 -29
- package/bin/lib/ark-order-facts.mjs +319 -17
- package/bin/lib/ark-order-sensors.mjs +28 -101
- package/bin/lib/ark-order-types.mjs +3 -2
- package/bin/lib/ark-run-doctor.mjs +17 -28
- package/bin/lib/ark-run-facts.mjs +461 -48
- package/bin/lib/ark-run-sensors.mjs +6 -4
- package/bin/lib/arkgate-pins.mjs +91 -0
- package/bin/lib/arkrule-file-hints.mjs +2 -47
- package/bin/lib/arkrules-contract.mjs +124 -133
- package/bin/lib/arkrules-sensors.mjs +55 -252
- package/bin/lib/check-args.mjs +85 -1
- package/bin/lib/ci-and-commands.mjs +24 -78
- package/bin/lib/ci-merge-boundary.mjs +1 -1
- package/bin/lib/class-source-scan.mjs +479 -0
- package/bin/lib/clone-detection.mjs +474 -0
- package/bin/lib/codex-home.mjs +5 -3
- package/bin/lib/config-contract-slices.mjs +739 -0
- package/bin/lib/config-contract.mjs +42 -607
- package/bin/lib/config-extras.mjs +16 -22
- package/bin/lib/config-warnings.mjs +32 -0
- package/bin/lib/contract-smells.mjs +33 -15
- package/bin/lib/core-ratchet.mjs +0 -2
- package/bin/lib/dashboard-cli.mjs +62 -0
- package/bin/lib/deep-module-coach.mjs +2 -4
- package/bin/lib/deploy-path.mjs +13 -38
- package/bin/lib/design-delta.mjs +1 -1
- package/bin/lib/design-smells.mjs +2 -2
- package/bin/lib/diagnostic-catalog.mjs +23 -9
- package/bin/lib/doctor-advisories.mjs +78 -8
- package/bin/lib/doctor-green-cite.mjs +1 -1
- package/bin/lib/doctor-human.mjs +1 -1
- package/bin/lib/doctor-next-actions.mjs +19 -4
- package/bin/lib/doctor-plan.mjs +9 -13
- package/bin/lib/duplication-io.mjs +572 -0
- package/bin/lib/effective-contract-load.mjs +282 -83
- package/bin/lib/enforcement-profiles.mjs +40 -4
- package/bin/lib/entry-points-io.mjs +424 -0
- package/bin/lib/extra-merge-teeth.mjs +36 -0
- package/bin/lib/field-install.mjs +38 -14
- package/bin/lib/first-run-help.mjs +10 -5
- package/bin/lib/flat-parent-importers.mjs +139 -0
- package/bin/lib/flat-parent-pilot.mjs +40 -100
- package/bin/lib/gate-files.mjs +58 -90
- package/bin/lib/git-change-scope.mjs +109 -0
- package/bin/lib/github-enforcement.mjs +166 -29
- package/bin/lib/golden-pattern.mjs +1 -1
- package/bin/lib/hook-templates.mjs +17 -15
- package/bin/lib/host-support-matrix.mjs +3 -1
- package/bin/lib/html-report-advisories.mjs +4 -0
- package/bin/lib/html-report-depth.mjs +2 -2
- package/bin/lib/html-report.mjs +2 -2
- package/bin/lib/import-graph-projection.mjs +112 -0
- package/bin/lib/import-resolve.mjs +13 -1
- package/bin/lib/improvement-compass-doctor.mjs +1 -24
- package/bin/lib/install-migrate.mjs +50 -73
- package/bin/lib/invariant-coverage-io.mjs +4 -3
- package/bin/lib/invariant-coverage.mjs +85 -11
- package/bin/lib/invariant-probe-cli.mjs +347 -0
- package/bin/lib/invariant-probe-io.mjs +165 -0
- package/bin/lib/invariant-probe-runner.mjs +268 -0
- package/bin/lib/invariant-probe-sites.mjs +174 -0
- package/bin/lib/invariant-probe-workspace.mjs +183 -0
- package/bin/lib/invariant-probe.mjs +461 -0
- package/bin/lib/invariant-tests-path.mjs +16 -22
- package/bin/lib/literal-path-drift.mjs +4 -2
- package/bin/lib/managed-upgrade.mjs +2 -8
- package/bin/lib/mcp-adoption.mjs +13 -6
- package/bin/lib/mcp-hook-payload.mjs +403 -63
- package/bin/lib/orphan-modules-io.mjs +458 -0
- package/bin/lib/orphan-modules.mjs +251 -0
- package/bin/lib/outside-importers.mjs +197 -0
- package/bin/lib/package-manager.mjs +60 -2
- package/bin/lib/parse-health.mjs +1 -1
- package/bin/lib/passthrough-commands.mjs +83 -0
- package/bin/lib/persistence-write-hint.mjs +106 -0
- package/bin/lib/pin-dependent-gates.mjs +117 -0
- package/bin/lib/policy-delta-io.mjs +219 -14
- package/bin/lib/post-green-path.mjs +1 -1
- package/bin/lib/prepare-change.mjs +115 -7
- package/bin/lib/prepare-write.mjs +2 -2
- package/bin/lib/presets.mjs +59 -73
- package/bin/lib/product-copy.mjs +1 -6
- package/bin/lib/project-root.mjs +31 -62
- package/bin/lib/remediation.mjs +23 -4
- package/bin/lib/resolved-candidate-facts.mjs +405 -117
- package/bin/lib/rules-inventory.mjs +22 -3
- package/bin/lib/rules-under-contract.mjs +247 -9
- package/bin/lib/scan-files.mjs +36 -4
- package/bin/lib/schema-validation.mjs +130 -0
- package/bin/lib/sensor-promote-cli.mjs +2 -11
- package/bin/lib/sensor-promote-io.mjs +33 -22
- package/bin/lib/sensor-promotion.mjs +1 -1
- package/bin/lib/skill-catalog-apply.mjs +1 -1
- package/bin/lib/skill-install.mjs +42 -38
- package/bin/lib/snippet-analysis.mjs +140 -6
- package/bin/lib/source-policy.mjs +34 -0
- package/bin/lib/start-install-recovery.mjs +72 -2
- package/bin/lib/start-preview.mjs +31 -1
- package/bin/lib/states-transitions-presence.mjs +3 -3
- package/bin/lib/status-command.mjs +35 -22
- package/bin/lib/status-manifest.mjs +50 -30
- package/bin/lib/status-transition-catalog.mjs +0 -6
- package/bin/lib/suggestions.mjs +3 -3
- package/bin/lib/team-parliament-io.mjs +76 -88
- package/bin/lib/team-parliament.mjs +21 -10
- package/bin/lib/upgrade-behavior-changes.mjs +2 -2
- package/bin/lib/upgrade-command.mjs +2 -24
- package/bin/lib/upgrade-outcome.mjs +1 -1
- package/bin/lib/upstream-report.mjs +21 -10
- package/bin/lib/violations.mjs +10 -9
- package/bin/lib/weakest-link.mjs +13 -0
- package/bin/lib/write-path-capabilities.mjs +6 -4
- package/dist/chunk-A5EIY3HI.cjs +5 -0
- package/dist/chunk-AHACEKSV.js +16 -0
- package/dist/chunk-SVSQVGFJ.js +5 -0
- package/dist/chunk-UN5EH4QV.cjs +16 -0
- package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-GPvvdleM.d.cts} +30 -4
- package/dist/configTypes-GPvvdleM.d.ts +257 -0
- package/dist/eslint/index.cjs +18 -5
- package/dist/eslint/index.d.cts +368 -0
- package/dist/eslint/index.d.ts +139 -156
- package/dist/eslint/index.js +11 -5
- package/dist/index.cjs +36 -35
- package/dist/index.d.cts +5547 -0
- package/dist/index.d.ts +3283 -198
- package/dist/index.js +36 -35
- 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-35mrwAHe.d.ts → runtime/index.d.cts} +828 -276
- package/dist/runtime/index.d.ts +2741 -133
- package/dist/runtime/index.js +13 -30
- package/dist/{types-Bdj32QwA.d.ts → types-CnauMQQL.d.cts} +607 -163
- package/dist/types-CnauMQQL.d.ts +1800 -0
- package/docs/README.md +3 -1
- package/docs/agent-guide.md +217 -16
- package/docs/ai-gates.md +174 -26
- package/docs/arkorder.md +32 -15
- package/docs/brownfield-adoption.md +5 -1
- package/docs/configuration.md +230 -31
- package/docs/develop.md +2 -1
- package/docs/diagnostics.md +212 -19
- package/docs/package-surface.md +35 -9
- package/docs/product-voice.md +7 -1
- package/package.json +69 -19
- package/schemas/ark.analysis-result.schema.json +15 -0
- package/schemas/ark.config.schema.json +77 -12
- package/schemas/ark.invariant-probe.schema.json +368 -0
- 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 +5 -1
- package/templates/agent-skills/ark-contract/SKILL.md +17 -0
- package/templates/agent-skills/ark-coverage/SKILL.md +2 -0
- package/templates/agent-skills/ark-explore/SKILL.md +7 -0
- package/templates/agent-skills/ark-fix/SKILL.md +20 -0
- package/templates/agent-skills/ark-order/SKILL.md +4 -2
- package/templates/agent-skills/ark-place/SKILL.md +8 -0
- 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 +5 -1
- package/templates/skills/ark-contract.md +17 -0
- package/templates/skills/ark-coverage.md +2 -0
- package/templates/skills/ark-explore.md +7 -0
- package/templates/skills/ark-fix.md +20 -0
- package/templates/skills/ark-order.md +4 -2
- package/templates/skills/ark-place.md +8 -0
- package/templates/skills/ark-runtime.md +6 -1
- package/bin/lib/ark-order-error.mjs +0 -18
- package/bin/lib/ark-order-invariants.mjs +0 -346
- package/dist/types-Ik_OakcI.d.ts +0 -183
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE — do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* Canonical algorithm: src/domain/persistenceWriteHint.ts
|
|
5
|
+
* Regenerate: node scripts/generate-cli-pure.mjs
|
|
6
|
+
* Drift check: node scripts/generate-cli-pure.mjs --check
|
|
7
|
+
*
|
|
8
|
+
* Pure CLI helper (bin/lib/persistence-write-hint.mjs). Zero Node I/O.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** IO / ORM import evidence. postgres and drizzle-orm include package subpaths. */
|
|
12
|
+
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)/;
|
|
13
|
+
/** Path-alias / local db module (`@/lib/db`) without resolving tsconfig. */
|
|
14
|
+
const IO_ALIAS_IMPORT_RE = /\bfrom\s+['"](?:@\/|~\/)?(?:[\w.-]+\/)*(?:db|database|prisma|drizzle|orm)(?:\.[cm]?[jt]sx?)?['"]|require\(\s*['"](?:@\/|~\/)?(?:[\w.-]+\/)*(?:db|database|prisma|drizzle|orm)/;
|
|
15
|
+
/** Named imports from a local persistence module (same module names as IO_ALIAS_IMPORT_RE). */
|
|
16
|
+
const LOCAL_CLIENT_IMPORT_RE = /\bimport\s+(?:type\s+)?\{([^}]*)\}\s*from\s*['"](?:@\/|~\/)?(?:[\w.-]+\/)*(?:db|database|prisma|drizzle|orm)(?:\.[cm]?[jt]sx?)?['"]/g;
|
|
17
|
+
/** Conventional client identifiers (PrismaClient included via the /i flag). */
|
|
18
|
+
const PERSISTENCE_CLIENT_NAMES = [
|
|
19
|
+
'db',
|
|
20
|
+
'tx',
|
|
21
|
+
'client',
|
|
22
|
+
'prisma',
|
|
23
|
+
'prismaClient',
|
|
24
|
+
'drizzle',
|
|
25
|
+
];
|
|
26
|
+
const WRITE_VERB_SOURCE = '(?:insert(?:One|Many)?|update(?:One|Many)?|upsert|delete(?:One|Many)?|createMany|create|replaceOne|findOneAnd(?:Update|Delete|Replace))';
|
|
27
|
+
const SQL_WRITE_RE = /\b(?:INSERT\s+INTO|(?<!\b(?:FOR(?:\s+NO\s+KEY)?|DO)\s)UPDATE(?:\s+ONLY)?(?!\s+(?:OF|SET)\b)|DELETE\s+FROM(?:\s+ONLY)?|MERGE\s+INTO|TRUNCATE(?:\s+TABLE)?(?:\s+ONLY)?)\s+(?:(?:(?:"[^"]*"|[A-Za-z_][\w$]*)\s*\.\s*)?(?:"[^"]*"|[A-Za-z_][\w$]*)|\$\{[^}]+\})/i;
|
|
28
|
+
const NOT_A_SQL_TAG = /^(?:await|return|throw|yield|new|typeof|void|delete|if|else|case|of|in|instanceof|async|function|class|const|let|var|import|export|from|as|default|type)$/;
|
|
29
|
+
const TAGGED_SQL_RE = /(^|[^\w$])([A-Za-z_$][\w$]*)\s*`((?:[^`\\]|\\[\s\S])*)`/g;
|
|
30
|
+
const UPSERT_RE = /\bINSERT\s+INTO\b[\s\S]*?\bON\s+CONFLICT\b[\s\S]*?\bDO\s+UPDATE\s+SET\b[^;`]*/gi;
|
|
31
|
+
/** Call arguments with one level of nested parentheses: `({ log: fn() })`. */
|
|
32
|
+
const CALL_ARGS_SOURCE = '\\((?:[^()]|\\([^()]*\\))*\\)';
|
|
33
|
+
/** Receivers built inline: `new PrismaClient(...)` / `drizzle(...)`. */
|
|
34
|
+
const INLINE_CLIENT_SOURCE = `\\bnew\\s+PrismaClient\\s*${CALL_ARGS_SOURCE}|\\bdrizzle\\s*${CALL_ARGS_SOURCE}`;
|
|
35
|
+
/**
|
|
36
|
+
* `const orm = new PrismaClient()` / `private orm = new PrismaClient()` /
|
|
37
|
+
* `this.orm = new PrismaClient()` / `const sql = postgres(url)` / `db = drizzle(pool)`.
|
|
38
|
+
*/
|
|
39
|
+
const DRIVER_BINDING_RE = /(?<![\w$])([A-Za-z_$][\w$]*)\s*[?!]?\s*(?::[^=;\n]+)?=(?![=>])\s*(?:await\s+)?(?:new\s+(?:PrismaClient|Pool|Client|MongoClient|Sequelize|Kysely|Knex)\b|(?:drizzle|knex|Knex|postgres)\s*\()/g;
|
|
40
|
+
/** `orm: PrismaClient` / `db: Kysely<DB>` / `tx: Prisma.TransactionClient` — params and fields. */
|
|
41
|
+
const DRIVER_ANNOTATION_RE = /(?<![\w$])([A-Za-z_$][\w$]*)\s*[?!]?\s*:\s*(?:Readonly\s*<\s*)?(?:Prisma\s*\.\s*TransactionClient|PrismaClient|Pool|PoolClient|MongoClient|Sequelize|Kysely|Knex|DataSource|EntityManager|NodePgDatabase|PostgresJsDatabase|BetterSQLite3Database|MySql2Database|LibSQLDatabase)\b/g;
|
|
42
|
+
function escapeRegExp(value) {
|
|
43
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
44
|
+
}
|
|
45
|
+
export function sourceImportsPersistenceDriverText(content) {
|
|
46
|
+
return IO_IMPORT_HINT_RE.test(content) || IO_ALIAS_IMPORT_RE.test(content);
|
|
47
|
+
}
|
|
48
|
+
function collectFirstGroup(re, content, names) {
|
|
49
|
+
const global = new RegExp(re.source, 'g');
|
|
50
|
+
let match;
|
|
51
|
+
while ((match = global.exec(content)) !== null) {
|
|
52
|
+
if (match[1])
|
|
53
|
+
names.add(match[1]);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Identifiers that hold a persistence client in this file (sorted, unique): bound to a
|
|
58
|
+
* driver constructor, annotated with a driver client type, or imported by name from a
|
|
59
|
+
* local persistence module.
|
|
60
|
+
*/
|
|
61
|
+
function persistenceClientBindings(content) {
|
|
62
|
+
const names = new Set();
|
|
63
|
+
collectFirstGroup(DRIVER_BINDING_RE, content, names);
|
|
64
|
+
collectFirstGroup(DRIVER_ANNOTATION_RE, content, names);
|
|
65
|
+
const imports = new RegExp(LOCAL_CLIENT_IMPORT_RE.source, 'g');
|
|
66
|
+
let match;
|
|
67
|
+
while ((match = imports.exec(content)) !== null) {
|
|
68
|
+
for (const element of (match[1] ?? '').split(',')) {
|
|
69
|
+
const parts = element.trim().replace(/^type\s+/, '').split(/\s+as\s+/);
|
|
70
|
+
const local = (parts[1] ?? parts[0] ?? '').trim();
|
|
71
|
+
if (/^[A-Za-z_$][\w$]*$/.test(local))
|
|
72
|
+
names.add(local);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return [...names].sort();
|
|
76
|
+
}
|
|
77
|
+
function blankPersistenceComments(content) {
|
|
78
|
+
return content.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|`(?:[^`\\]|\\[\s\S])*`|'(?:[^'\\]|\\[\s\S])*'|"(?:[^"\\]|\\[\s\S])*"/g, (token) => token.charCodeAt(0) === 47
|
|
79
|
+
? token.replace(/[^\n]/g, ' ')
|
|
80
|
+
: token.charCodeAt(0) === 96
|
|
81
|
+
? token.replace(/--[^\n`]*/g, (comment) => ' '.repeat(comment.length))
|
|
82
|
+
: token);
|
|
83
|
+
}
|
|
84
|
+
export function sourceHasTaggedSqlWrite(content) {
|
|
85
|
+
const text = blankPersistenceComments(content);
|
|
86
|
+
TAGGED_SQL_RE.lastIndex = 0;
|
|
87
|
+
let match;
|
|
88
|
+
while ((match = TAGGED_SQL_RE.exec(text)) !== null) {
|
|
89
|
+
if (NOT_A_SQL_TAG.test(match[2] ?? ''))
|
|
90
|
+
continue;
|
|
91
|
+
if (SQL_WRITE_RE.test((match[3] ?? '').replace(UPSERT_RE, ' ')))
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
/** Receiver-bound write evidence (ORM verb on a persistence client, or raw SQL write). */
|
|
97
|
+
export function sourceHasPersistenceWrite(content) {
|
|
98
|
+
const text = blankPersistenceComments(content);
|
|
99
|
+
const receivers = [...PERSISTENCE_CLIENT_NAMES, ...persistenceClientBindings(text)]
|
|
100
|
+
.map(escapeRegExp)
|
|
101
|
+
.join('|');
|
|
102
|
+
const re = new RegExp(`(?:\\b(?:${receivers})(?![\\w$])|${INLINE_CLIENT_SOURCE})(?:\\s*\\.\\s*[A-Za-z_]\\w*)*\\s*\\.\\s*${WRITE_VERB_SOURCE}\\s*\\(`, 'i');
|
|
103
|
+
if (re.test(text))
|
|
104
|
+
return true;
|
|
105
|
+
return SQL_WRITE_RE.test(text);
|
|
106
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated gate files whose Ark command only resolves through a local arkgate pin.
|
|
3
|
+
*
|
|
4
|
+
* `npx ark-check`, `pnpm exec arkgate-mcp` and `yarn ark-check` reach arkgate's bins only
|
|
5
|
+
* when package.json declares arkgate. Without that pin the same line asks npm for a package
|
|
6
|
+
* named `ark-check` (404) or runs a stale global copy. The pinned form
|
|
7
|
+
* (`npx -y -p arkgate@<exact>`) does not depend on the pin and is not reported.
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { npxArkgatePrefixLength } from './package-manager.mjs';
|
|
12
|
+
|
|
13
|
+
const ARK_BIN = /^(?:arkgate-check|arkgate-mcp|arkgate|ark-check|ark-mcp|ark)$/;
|
|
14
|
+
const LOCAL_RUNNER_TEXT =
|
|
15
|
+
/(?:^|[\s"'`;&|(])(?:npx|pnpm(?:\s+--config\.verify-deps-before-run=false)?\s+exec|yarn)\s+(?:arkgate-check|arkgate-mcp|arkgate|ark-check|ark-mcp|ark)(?=[\s"'`;&|)]|$)/m;
|
|
16
|
+
const TOML_SERVER = /command\s*=\s*"([^"]*)"\s*\r?\nargs\s*=\s*\[([^\]]*)\]/g;
|
|
17
|
+
|
|
18
|
+
/** Host hooks, package scripts, and MCP registrations Ark generates. */
|
|
19
|
+
const PIN_DEPENDENT_CANDIDATES = [
|
|
20
|
+
'.claude/settings.json',
|
|
21
|
+
'.codex/hooks.json',
|
|
22
|
+
'.cursor/hooks.json',
|
|
23
|
+
'.grok/hooks/ark-write-gate.json',
|
|
24
|
+
'.agents/hooks.json',
|
|
25
|
+
'.mcp.json',
|
|
26
|
+
'.cursor/mcp.json',
|
|
27
|
+
'.agents/mcp_config.json',
|
|
28
|
+
'opencode.json',
|
|
29
|
+
'.codex/config.toml',
|
|
30
|
+
'.grok/config.toml',
|
|
31
|
+
'package.json',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function readText(file) {
|
|
35
|
+
try {
|
|
36
|
+
return fs.readFileSync(file, 'utf8');
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** True when this argv runs an Ark bin through a runner that needs the local pin. */
|
|
43
|
+
function argvDependsOnArkgatePin(command, args) {
|
|
44
|
+
if (typeof command !== 'string' || !Array.isArray(args)) return false;
|
|
45
|
+
const runner = path.basename(command.trim().replace(/\\/g, '/')).replace(/\.(?:cmd|exe)$/i, '');
|
|
46
|
+
if (runner === 'npx') return npxArkgatePrefixLength(args) === 0 && ARK_BIN.test(args[0] ?? '');
|
|
47
|
+
if (runner === 'yarn') return ARK_BIN.test(args[0] ?? '');
|
|
48
|
+
if (runner === 'pnpm') {
|
|
49
|
+
const exec = args.indexOf('exec');
|
|
50
|
+
return exec >= 0 && ARK_BIN.test(args[exec + 1] ?? '');
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function jsonServersDependOnPin(text) {
|
|
56
|
+
let json;
|
|
57
|
+
try {
|
|
58
|
+
json = JSON.parse(text);
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const servers = [json?.mcpServers?.ark, json?.servers?.ark].filter(
|
|
63
|
+
(server) => server && typeof server === 'object'
|
|
64
|
+
);
|
|
65
|
+
if (servers.some((server) => argvDependsOnArkgatePin(server.command, server.args ?? []))) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
const argv = json?.mcp?.ark?.command;
|
|
69
|
+
return Array.isArray(argv) && argvDependsOnArkgatePin(argv[0], argv.slice(1));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function tomlServersDependOnPin(text) {
|
|
73
|
+
for (const match of text.matchAll(TOML_SERVER)) {
|
|
74
|
+
const args = [...match[2].matchAll(/"([^"]*)"/g)].map((entry) => entry[1]);
|
|
75
|
+
if (argvDependsOnArkgatePin(match[1], args)) return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function fileDependsOnPin(relativePath, text) {
|
|
81
|
+
if (LOCAL_RUNNER_TEXT.test(text)) return true;
|
|
82
|
+
if (relativePath.endsWith('.toml')) return tomlServersDependOnPin(text);
|
|
83
|
+
if (relativePath.endsWith('.json')) return jsonServersDependOnPin(text);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function workflowFiles(root) {
|
|
88
|
+
const dir = path.join(root, '.github', 'workflows');
|
|
89
|
+
try {
|
|
90
|
+
return fs
|
|
91
|
+
.readdirSync(dir)
|
|
92
|
+
.filter((name) => /\.ya?ml$/i.test(name))
|
|
93
|
+
.sort()
|
|
94
|
+
.map((name) => `.github/workflows/${name}`);
|
|
95
|
+
} catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Project-relative generated files whose Ark command needs arkgate in package.json. */
|
|
101
|
+
export function pinDependentGateFiles(root) {
|
|
102
|
+
const found = [];
|
|
103
|
+
for (const rel of [...PIN_DEPENDENT_CANDIDATES, ...workflowFiles(root)]) {
|
|
104
|
+
const text = readText(path.join(root, rel));
|
|
105
|
+
if (text && fileDependsOnPin(rel, text)) found.push(rel);
|
|
106
|
+
}
|
|
107
|
+
return found;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Attach `dependentGateFiles` to a PACKAGE_PIN_ABSENT truth so doctor can lead with the pin
|
|
112
|
+
* when a generated file would 404 without it. Other codes pass through unchanged.
|
|
113
|
+
*/
|
|
114
|
+
export function withPinDependentGateFiles(root, truth) {
|
|
115
|
+
if (truth?.code !== 'PACKAGE_PIN_ABSENT') return truth;
|
|
116
|
+
return { ...truth, dependentGateFiles: pinDependentGateFiles(root) };
|
|
117
|
+
}
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { analyzePolicyDelta } from './analysis-engine.mjs';
|
|
5
|
-
import {
|
|
4
|
+
import { analyzePolicyDelta, stableSerialize } from './analysis-engine.mjs';
|
|
5
|
+
import { loadArkConfigContract } from './config-contract.mjs';
|
|
6
|
+
import {
|
|
7
|
+
directoryArkRulesReader,
|
|
8
|
+
loadEffectiveArkRules,
|
|
9
|
+
loadEffectiveArkRulesFromDisk,
|
|
10
|
+
normalizeProjectRelativePath,
|
|
11
|
+
objectArkRulesReader,
|
|
12
|
+
} from './effective-contract-load.mjs';
|
|
6
13
|
import {
|
|
7
14
|
coverageOptionsFromConfig,
|
|
8
15
|
invariantIdsFromCatalog,
|
|
9
16
|
loadInvariantCoverageInputs,
|
|
10
17
|
} from './invariant-coverage-io.mjs';
|
|
11
18
|
import { evaluateInvariantCoverage } from './invariant-coverage.mjs';
|
|
19
|
+
import { attachProbeEvidence } from './invariant-probe-io.mjs';
|
|
12
20
|
import { attachPolicyAdrNote } from './adr-path.mjs';
|
|
21
|
+
import { collectGovernedFiles } from './scan-files.mjs';
|
|
13
22
|
|
|
14
23
|
function readJsonFile(filePath, label) {
|
|
15
24
|
if (!fs.existsSync(filePath)) throw new Error(`${label} not found: ${filePath}`);
|
|
@@ -85,16 +94,72 @@ function configPathInRepository(root, configPath, top) {
|
|
|
85
94
|
return relative;
|
|
86
95
|
}
|
|
87
96
|
|
|
97
|
+
/**
|
|
98
|
+
* ArkRules files at the base ref, relative to the project root inside the repository.
|
|
99
|
+
* A path absent at the ref (added by the candidate) reads as `missing`.
|
|
100
|
+
*/
|
|
101
|
+
function baseRefSourceFiles(top, ref, prefix) {
|
|
102
|
+
const shown = runGit(top, ['ls-tree', '-r', '--name-only', ref]);
|
|
103
|
+
if (shown.status !== 0) return [];
|
|
104
|
+
const rootPrefix = prefix ? `${prefix}/` : '';
|
|
105
|
+
const files = [];
|
|
106
|
+
for (const line of shown.stdout.split('\n')) {
|
|
107
|
+
if (!line) continue;
|
|
108
|
+
if (rootPrefix && !line.startsWith(rootPrefix)) continue;
|
|
109
|
+
const rel = rootPrefix ? line.slice(rootPrefix.length) : line;
|
|
110
|
+
if (/\.[cm]?[tj]sx?$/.test(rel) && !rel.endsWith('.d.ts')) files.push(rel);
|
|
111
|
+
}
|
|
112
|
+
return files;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function baseRefArkRulesReader(root, top, ref) {
|
|
116
|
+
const prefix = path
|
|
117
|
+
.relative(fs.realpathSync(top), fs.realpathSync(root))
|
|
118
|
+
.split(path.sep)
|
|
119
|
+
.join('/');
|
|
120
|
+
const spec = (rel) => `${ref}:${prefix ? `${prefix}/` : ''}${rel}`;
|
|
121
|
+
return {
|
|
122
|
+
files: baseRefSourceFiles(top, ref, prefix),
|
|
123
|
+
readArkRulesFile: (rel) => {
|
|
124
|
+
const shown = runGit(top, ['show', spec(rel)]);
|
|
125
|
+
if (shown.status === 0) return { ok: true, content: shown.stdout };
|
|
126
|
+
const exists = runGit(top, ['cat-file', '-e', spec(rel)]);
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
missing: exists.status !== 0 && !shown.error,
|
|
130
|
+
message: `Cannot read policy base ArkRules ${spec(rel)}: ${
|
|
131
|
+
shown.stderr?.trim() || shown.error?.message || 'git show failed'
|
|
132
|
+
}`,
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
88
138
|
export function resolvePolicyBaseConfig({
|
|
89
139
|
root,
|
|
90
140
|
configPath,
|
|
91
141
|
basePath,
|
|
92
142
|
baseRef,
|
|
143
|
+
adoptMissingBase = false,
|
|
93
144
|
env = process.env,
|
|
94
145
|
}) {
|
|
95
146
|
if (basePath) {
|
|
96
147
|
const absolute = path.isAbsolute(basePath) ? basePath : path.resolve(root, basePath);
|
|
97
|
-
|
|
148
|
+
const config = readJsonFile(absolute, 'Policy base');
|
|
149
|
+
// A base config FILE carries its ArkRules next to it (same relative paths as the
|
|
150
|
+
// project), never from the candidate working tree: that would compare the
|
|
151
|
+
// candidate catalog with itself and hide every demotion/deletion.
|
|
152
|
+
const baseRoot = fs.realpathSync(path.dirname(absolute));
|
|
153
|
+
const include = Array.isArray(config.include) && config.include.length > 0 ? config.include : ['src'];
|
|
154
|
+
return {
|
|
155
|
+
config,
|
|
156
|
+
source: absolute,
|
|
157
|
+
ref: null,
|
|
158
|
+
files: collectGovernedFiles(baseRoot, { ...config, include })
|
|
159
|
+
.map((file) => path.relative(baseRoot, file).split(path.sep).join('/'))
|
|
160
|
+
.filter((rel) => rel && !rel.startsWith('..')),
|
|
161
|
+
readArkRulesFile: directoryArkRulesReader(baseRoot),
|
|
162
|
+
};
|
|
98
163
|
}
|
|
99
164
|
|
|
100
165
|
const envRef = normalizePolicyBaseRef(env.ARK_POLICY_BASE_REF);
|
|
@@ -119,7 +184,8 @@ export function resolvePolicyBaseConfig({
|
|
|
119
184
|
// A newly adopted contract has no predecessor to weaken. CI-provided and
|
|
120
185
|
// auto-discovered bases may therefore omit the config; an explicit CLI ref
|
|
121
186
|
// remains fail-closed because the caller asked to compare that exact input.
|
|
122
|
-
|
|
187
|
+
// A team base bound by --base / discovery (adoptMissingBase) is not such a request.
|
|
188
|
+
if (refExists.status === 0 && (!baseRef || adoptMissingBase)) return null;
|
|
123
189
|
if (requestedRef) {
|
|
124
190
|
throw new Error(
|
|
125
191
|
`Cannot read policy base ${ref}:${relativeConfig}: ${result.stderr.trim() || 'git show failed'}`
|
|
@@ -132,6 +198,7 @@ export function resolvePolicyBaseConfig({
|
|
|
132
198
|
config: JSON.parse(result.stdout),
|
|
133
199
|
source: `git:${ref}:${relativeConfig}`,
|
|
134
200
|
ref,
|
|
201
|
+
...baseRefArkRulesReader(root, top, ref),
|
|
135
202
|
};
|
|
136
203
|
} catch (error) {
|
|
137
204
|
throw new Error(
|
|
@@ -148,6 +215,135 @@ export function readPolicyAcknowledgement(root, acknowledgementPath) {
|
|
|
148
215
|
return readJsonFile(absolute, 'Policy acknowledgement');
|
|
149
216
|
}
|
|
150
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Effective ArkRules of the policy base. A path the base config references but the
|
|
220
|
+
* base ref does not contain yet counts as an empty layer (git refs only). Any other
|
|
221
|
+
* failure is fail-closed: silently dropping the base catalog would make every
|
|
222
|
+
* ArkRule demotion or deletion classify as neutral.
|
|
223
|
+
*/
|
|
224
|
+
function loadBaseArkRules(base) {
|
|
225
|
+
const loaded = loadEffectiveArkRules(base.config, base.readArkRulesFile, {
|
|
226
|
+
missingAsEmpty: base.ref != null,
|
|
227
|
+
...(Array.isArray(base.files) ? { files: base.files } : {}),
|
|
228
|
+
});
|
|
229
|
+
if (loaded.errors.length > 0) {
|
|
230
|
+
const message = loaded.errors.map((issue) => `- ${issue.path}: ${issue.message}`).join('\n');
|
|
231
|
+
const remedy =
|
|
232
|
+
base.ref == null
|
|
233
|
+
? `\nA --policy-base file reads its ArkRules next to it: copy the base catalog to the same relative paths under ${path.dirname(base.source)}, or compare against a git ref with --policy-base-ref.`
|
|
234
|
+
: '';
|
|
235
|
+
throw new Error(
|
|
236
|
+
`Policy base ArkRules could not be loaded (${base.source}); ArkRules transitions cannot be classified:\n${message}${remedy}`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return loaded.arkRules;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function hasArkRulesMap(config) {
|
|
243
|
+
const refs = config?.arkRules;
|
|
244
|
+
return Boolean(refs && typeof refs === 'object' && Object.keys(refs).length > 0);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Paths in a supplied catalog. A slice rule file is enough for the child wall to name it. */
|
|
248
|
+
function catalogPaths(files) {
|
|
249
|
+
if (!files || typeof files !== 'object') return [];
|
|
250
|
+
const out = [];
|
|
251
|
+
for (const key of Object.keys(files)) {
|
|
252
|
+
const rel = normalizeProjectRelativePath(String(key));
|
|
253
|
+
if (rel) out.push(rel);
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function loadedOrThrow(loaded, label) {
|
|
259
|
+
if (loaded.errors.length === 0) return loaded.arkRules;
|
|
260
|
+
const message = loaded.errors.map((issue) => `- ${issue.path}: ${issue.message}`).join('\n');
|
|
261
|
+
throw new Error(`${label} ArkRules could not be loaded:\n${message}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function normalizedArkRulesMap(config) {
|
|
265
|
+
let normalized = config;
|
|
266
|
+
try {
|
|
267
|
+
normalized = loadArkConfigContract(config, 'candidateConfig').config;
|
|
268
|
+
} catch {
|
|
269
|
+
// An invalid candidate is reported by analyzePolicyDelta; compare it as given.
|
|
270
|
+
}
|
|
271
|
+
return stableSerialize(hasArkRulesMap(normalized) ? normalized.arkRules : null);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* True when a supplied `candidateConfig` names the same ArkRules catalog files as the
|
|
276
|
+
* loaded project config, so the candidate catalog is the one on disk. Both sides are
|
|
277
|
+
* normalised first: the project config in memory carries loader defaults (`$schema`,
|
|
278
|
+
* upgraded `schemaVersion`), so passing `ark.config.json` verbatim must still match.
|
|
279
|
+
*/
|
|
280
|
+
export function candidateArkRulesMatchProject(candidateConfig, projectConfig) {
|
|
281
|
+
if (candidateConfig === undefined) return true;
|
|
282
|
+
if (!candidateConfig || typeof candidateConfig !== 'object' || Array.isArray(candidateConfig)) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
return normalizedArkRulesMap(candidateConfig) === normalizedArkRulesMap(projectConfig);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* MCP `ark_policy_delta`: Effective ArkRules for both sides of a config transition.
|
|
290
|
+
* Base ArkRules must be SUPPLIED as data (`baseArkRuleFiles`, keyed by the project-
|
|
291
|
+
* relative paths in `baseConfig.arkRules`) whenever the base config maps any: the
|
|
292
|
+
* server only sees the candidate working tree, and reading the base catalog from it
|
|
293
|
+
* would classify every ArkRule demotion or deletion as neutral. The candidate comes
|
|
294
|
+
* from `candidateArkRuleFiles`, or from disk when the candidate is the project config.
|
|
295
|
+
*/
|
|
296
|
+
export function resolvePolicyDeltaArkRules({
|
|
297
|
+
root,
|
|
298
|
+
baseConfig,
|
|
299
|
+
candidateConfig,
|
|
300
|
+
candidateIsProjectConfig,
|
|
301
|
+
baseArkRuleFiles,
|
|
302
|
+
candidateArkRuleFiles,
|
|
303
|
+
}) {
|
|
304
|
+
let baseArkRules;
|
|
305
|
+
if (hasArkRulesMap(baseConfig)) {
|
|
306
|
+
if (!baseArkRuleFiles || typeof baseArkRuleFiles !== 'object') {
|
|
307
|
+
throw new Error(
|
|
308
|
+
'baseConfig maps arkRules, so the ArkRules transition cannot be classified without the base catalog: ' +
|
|
309
|
+
'pass baseArkRuleFiles as { "<path from baseConfig.arkRules>": <ArkRules file JSON> }.'
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
baseArkRules = loadedOrThrow(
|
|
313
|
+
loadEffectiveArkRules(baseConfig, objectArkRulesReader(baseArkRuleFiles), {
|
|
314
|
+
files: catalogPaths(baseArkRuleFiles),
|
|
315
|
+
}),
|
|
316
|
+
'Base'
|
|
317
|
+
);
|
|
318
|
+
} else {
|
|
319
|
+
baseArkRules = loadEffectiveArkRules(baseConfig, () => ({ ok: false, message: '' })).arkRules;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let candidateArkRules;
|
|
323
|
+
if (candidateArkRuleFiles && typeof candidateArkRuleFiles === 'object') {
|
|
324
|
+
candidateArkRules = loadedOrThrow(
|
|
325
|
+
loadEffectiveArkRules(candidateConfig, objectArkRulesReader(candidateArkRuleFiles), {
|
|
326
|
+
files: catalogPaths(candidateArkRuleFiles),
|
|
327
|
+
}),
|
|
328
|
+
'Candidate'
|
|
329
|
+
);
|
|
330
|
+
} else if (candidateIsProjectConfig) {
|
|
331
|
+
candidateArkRules = loadedOrThrow(
|
|
332
|
+
loadEffectiveArkRulesFromDisk(root, candidateConfig),
|
|
333
|
+
'Candidate'
|
|
334
|
+
);
|
|
335
|
+
} else if (hasArkRulesMap(candidateConfig)) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
'candidateConfig maps different arkRules files than the project contract: pass candidateArkRuleFiles ' +
|
|
338
|
+
'as { "<path from candidateConfig.arkRules>": <ArkRules file JSON> }.'
|
|
339
|
+
);
|
|
340
|
+
} else {
|
|
341
|
+
candidateArkRules = loadEffectiveArkRules(candidateConfig, () => ({ ok: false, message: '' }))
|
|
342
|
+
.arkRules;
|
|
343
|
+
}
|
|
344
|
+
return { baseArkRules, candidateArkRules };
|
|
345
|
+
}
|
|
346
|
+
|
|
151
347
|
export function analyzePolicyTransition({
|
|
152
348
|
root,
|
|
153
349
|
configPath,
|
|
@@ -155,18 +351,21 @@ export function analyzePolicyTransition({
|
|
|
155
351
|
strictMerge,
|
|
156
352
|
basePath,
|
|
157
353
|
baseRef,
|
|
354
|
+
adoptMissingBase = false,
|
|
158
355
|
acknowledgementPath,
|
|
159
356
|
}) {
|
|
160
357
|
if (!strictMerge && !basePath && !baseRef && !acknowledgementPath) return undefined;
|
|
161
|
-
const base = resolvePolicyBaseConfig({ root, configPath, basePath, baseRef });
|
|
162
|
-
if (!base && (basePath || baseRef || acknowledgementPath)) {
|
|
358
|
+
const base = resolvePolicyBaseConfig({ root, configPath, basePath, baseRef, adoptMissingBase });
|
|
359
|
+
if (!base && (basePath || (baseRef && !adoptMissingBase) || acknowledgementPath)) {
|
|
163
360
|
throw new Error('Policy delta was requested but no policy base could be resolved.');
|
|
164
361
|
}
|
|
165
362
|
if (!base) return undefined;
|
|
166
363
|
|
|
167
364
|
// AR02/AR11: load Effective ArkRules so mode edits inside arkrules/*.json classify,
|
|
168
365
|
// and attach candidate coverage so covered advisory→enforced can auto-allow.
|
|
169
|
-
|
|
366
|
+
// Base ArkRules come from the SAME source as the base config (git ref or the
|
|
367
|
+
// base file's directory), never from the candidate working tree.
|
|
368
|
+
const baseArkRules = loadBaseArkRules(base);
|
|
170
369
|
const candidateLoad = loadEffectiveArkRulesFromDisk(root, candidateConfig);
|
|
171
370
|
if (candidateLoad.errors.length > 0) {
|
|
172
371
|
const message = candidateLoad.errors
|
|
@@ -174,8 +373,6 @@ export function analyzePolicyTransition({
|
|
|
174
373
|
.join('\n');
|
|
175
374
|
throw new Error(`Invalid candidate Effective Contract:\n${message}`);
|
|
176
375
|
}
|
|
177
|
-
// Base load errors: best-effort empty (base git tree may lack arkrules files on disk).
|
|
178
|
-
const baseArkRules = baseLoad.errors.length > 0 ? undefined : baseLoad.arkRules;
|
|
179
376
|
const candidateArkRules = candidateLoad.arkRules;
|
|
180
377
|
|
|
181
378
|
let candidateInvariantCoverage;
|
|
@@ -189,12 +386,20 @@ export function analyzePolicyTransition({
|
|
|
189
386
|
fileContents: coverageInputs.fileContents,
|
|
190
387
|
testFiles: coverageInputs.testFiles,
|
|
191
388
|
testGlobsMissing: coverageInputs.testGlobsMissing,
|
|
192
|
-
// No coverageStats / coverageRoots: this caller reads coverage ROWS and
|
|
193
|
-
// drops the violations, and both only shape violation messages. Passing
|
|
194
|
-
// them would look like wiring while changing nothing observable here.
|
|
195
389
|
coverageBudgetExhausted: coverageInputs.coverageBudgetExhausted === true,
|
|
390
|
+
...(coverageInputs.stats ? { coverageStats: coverageInputs.stats } : {}),
|
|
391
|
+
// The declared roots set `coverageRootsDeclared` / `outsideDeclaredRoots`
|
|
392
|
+
// on each row, and canPromoteInvariant refuses without them. Same inputs
|
|
393
|
+
// as `--promote` (sensor-promote-io), so both surfaces judge alike.
|
|
394
|
+
...(coverageInputs.coverageRoots ? { coverageRoots: coverageInputs.coverageRoots } : {}),
|
|
196
395
|
});
|
|
197
|
-
|
|
396
|
+
// ADR 0039: the same committed probe evidence --promote reads, so both
|
|
397
|
+
// surfaces refuse alike. Read-only; nothing here runs a test.
|
|
398
|
+
candidateInvariantCoverage = attachProbeEvidence(
|
|
399
|
+
root,
|
|
400
|
+
evaluated.coverage,
|
|
401
|
+
candidateArkRules.invariants
|
|
402
|
+
).rows;
|
|
198
403
|
}
|
|
199
404
|
|
|
200
405
|
const acknowledgement = readPolicyAcknowledgement(root, acknowledgementPath);
|
|
@@ -205,7 +410,7 @@ export function analyzePolicyTransition({
|
|
|
205
410
|
acknowledgement,
|
|
206
411
|
baseSource: base.source,
|
|
207
412
|
candidateSource: path.isAbsolute(configPath) ? configPath : path.join(root, configPath),
|
|
208
|
-
|
|
413
|
+
baseArkRules,
|
|
209
414
|
candidateArkRules,
|
|
210
415
|
...(candidateInvariantCoverage ? { candidateInvariantCoverage } : {}),
|
|
211
416
|
}),
|
|
@@ -21,7 +21,7 @@ export const POST_GREEN_PRIMARY_SKILL = '/ark-explore';
|
|
|
21
21
|
export const POST_GREEN_PRIMARY_ACTION = POST_GREEN_HUMAN;
|
|
22
22
|
|
|
23
23
|
/** Short label for tables / metrics. */
|
|
24
|
-
|
|
24
|
+
const POST_GREEN_PRIMARY_SHORT =
|
|
25
25
|
'/ark-explore shape-focus → /ark-autopilot (apply one B pilot) # Shape residual';
|
|
26
26
|
|
|
27
27
|
/** Placement coaching while design residual remains (new code only). */
|