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
|
@@ -6,6 +6,9 @@ import path from 'node:path';
|
|
|
6
6
|
/** Kill hung gh instead of stalling CI. */
|
|
7
7
|
export const SPAWN_TIMEOUT_MS = 8000;
|
|
8
8
|
|
|
9
|
+
/** GitHub Actions app id: a required check bound to it can only be reported by a workflow job. */
|
|
10
|
+
export const GITHUB_ACTIONS_APP_ID = 15368;
|
|
11
|
+
|
|
9
12
|
function runGh(args, opts = {}) {
|
|
10
13
|
const { run, ...rest } = opts;
|
|
11
14
|
const spawn = typeof run === 'function' ? run : spawnSync;
|
|
@@ -18,11 +21,11 @@ function runGh(args, opts = {}) {
|
|
|
18
21
|
|
|
19
22
|
const IF_LINE = /^[ \t]*(?:-\s+)?(?:"if"|'if'|if):\s*(.*?)\s*(?:#.*)?$/i;
|
|
20
23
|
const CONTINUE_LINE = /^[ \t]*(?:-\s+)?(?:"continue-on-error"|'continue-on-error'|continue-on-error):\s*(.*?)\s*(?:#.*)?$/i;
|
|
21
|
-
const SAFE_IF = /^(?:['"]?true['"]?|['"]
|
|
22
|
-
const SAFE_NEEDS_IF = /^['"]
|
|
24
|
+
const SAFE_IF = /^(?:['"]?true['"]?|['"]?(?:\$\{\{\s*(?:true|always\(\))\s*\}\}|always\(\))['"]?)$/i;
|
|
25
|
+
const SAFE_NEEDS_IF = /^['"]?(?:\$\{\{\s*always\(\)\s*\}\}|always\(\))['"]?$/i;
|
|
23
26
|
const SAFE_CONTINUE = /^['"]?false['"]?$/i;
|
|
24
27
|
const ENV_PREFIX = /^(?:(?:env\s+)?(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S+)\s+)*)/;
|
|
25
|
-
const DIRECT_ARK = /^(?:(?:npx
|
|
28
|
+
const DIRECT_ARK = /^(?:(?:npx(?:\s+(?:-y|--yes))?(?:\s+(?:-p\s+|--package[\s=])arkgate(?:@\S+)?)?|yarn)\s+(?:arkgate-check|ark-check)(?=\s|$)|pnpm(?:\s+--\S+)*\s+exec\s+(?:arkgate-check|ark-check)(?=\s|$)|node\s+(?:\S+\/)?bin\/(?:arkgate-check|ark-check)\.mjs(?=\s|$)|(?:arkgate-check|ark-check)(?=\s|$))/;
|
|
26
29
|
const CHECK_SCRIPT = /^(?:(?:npm|pnpm)\s+run\s+check:architecture(?=\s|$)|yarn(?:\s+run)?\s+check:architecture(?=\s|$))/;
|
|
27
30
|
const FAIL_CLOSED = /(?:^|\s)--(?:strict|strict-merge|require-gates)(?=\s|$)/;
|
|
28
31
|
|
|
@@ -164,7 +167,7 @@ function propertyValue(line, pattern) {
|
|
|
164
167
|
return line.match(pattern)?.[1]?.trim() ?? null;
|
|
165
168
|
}
|
|
166
169
|
|
|
167
|
-
function
|
|
170
|
+
function controlValues(lines, propertyIndent) {
|
|
168
171
|
const atIndent = lines.filter((line) => {
|
|
169
172
|
const indent = line.match(/^\s*/)?.[0].length ?? 0;
|
|
170
173
|
return indent + (/^\s*-\s+/.test(line) ? 2 : 0) === propertyIndent;
|
|
@@ -173,9 +176,13 @@ function unsafeControl(lines, propertyIndent, needs = false) {
|
|
|
173
176
|
const continuation = atIndent
|
|
174
177
|
.map((line) => propertyValue(line, CONTINUE_LINE))
|
|
175
178
|
.find((value) => value !== null);
|
|
179
|
+
return { condition, continuation };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function unsafeControl(lines, propertyIndent) {
|
|
183
|
+
const { condition, continuation } = controlValues(lines, propertyIndent);
|
|
176
184
|
if (condition !== undefined && !SAFE_IF.test(condition)) return true;
|
|
177
|
-
|
|
178
|
-
return needs && (condition === undefined || !SAFE_NEEDS_IF.test(condition));
|
|
185
|
+
return continuation !== undefined && !SAFE_CONTINUE.test(continuation);
|
|
179
186
|
}
|
|
180
187
|
|
|
181
188
|
function stepBounds(lines, runIndex, runIndent, inline) {
|
|
@@ -217,7 +224,7 @@ function unquoteRun(value) {
|
|
|
217
224
|
return trimmed;
|
|
218
225
|
}
|
|
219
226
|
|
|
220
|
-
|
|
227
|
+
function workflowRunText(text) {
|
|
221
228
|
const lines = String(text || '').split('\n');
|
|
222
229
|
const commands = [];
|
|
223
230
|
for (let index = 0; index < lines.length; index++) {
|
|
@@ -243,14 +250,90 @@ export function workflowRunText(text) {
|
|
|
243
250
|
return commands.join('\n');
|
|
244
251
|
}
|
|
245
252
|
|
|
246
|
-
|
|
253
|
+
const NEEDS_LINE = /^\s*(?:"needs"|'needs'|needs):\s*(.*?)\s*(?:#.*)?$/;
|
|
254
|
+
const JOB_ID = /^[A-Za-z0-9_-]+$/;
|
|
255
|
+
|
|
256
|
+
function unquoteScalar(value) {
|
|
257
|
+
return String(value).trim().replace(/^(['"])(.*)\1$/, '$2').trim();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** `needs:` as scalar, flow list, or block list. `valid: false` for expressions/malformed. */
|
|
261
|
+
function jobNeeds(job) {
|
|
247
262
|
const propertyIndent = jobPropertyIndent(job);
|
|
248
263
|
const lines = job.body.split('\n');
|
|
249
|
-
const
|
|
250
|
-
(line.match(/^\s*/)?.[0].length ?? 0) === propertyIndent &&
|
|
251
|
-
/^\s*(?:"needs"|'needs'|needs):/.test(line)
|
|
264
|
+
const start = lines.findIndex((line) =>
|
|
265
|
+
(line.match(/^\s*/)?.[0].length ?? 0) === propertyIndent && NEEDS_LINE.test(line)
|
|
252
266
|
);
|
|
253
|
-
return
|
|
267
|
+
if (start < 0) return { present: false, ids: [], valid: true };
|
|
268
|
+
const value = unquoteScalar(lines[start].match(NEEDS_LINE)[1]);
|
|
269
|
+
if (value) {
|
|
270
|
+
const raw = value.startsWith('[') && value.endsWith(']') ? value.slice(1, -1).split(',') : [value];
|
|
271
|
+
const ids = raw.map(unquoteScalar).filter((id) => JOB_ID.test(id));
|
|
272
|
+
return { present: true, ids, valid: ids.length > 0 && ids.length === raw.length };
|
|
273
|
+
}
|
|
274
|
+
const ids = [];
|
|
275
|
+
for (let index = start + 1; index < lines.length; index++) {
|
|
276
|
+
if (!lines[index].trim() || /^\s*#/.test(lines[index])) continue;
|
|
277
|
+
if ((lines[index].match(/^\s*/)?.[0].length ?? 0) <= propertyIndent) break;
|
|
278
|
+
const item = lines[index].match(/^\s*-\s*(['"]?)([A-Za-z0-9_-]+)\1\s*(?:#.*)?$/);
|
|
279
|
+
if (!item) return { present: true, ids: [], valid: false };
|
|
280
|
+
ids.push(item[2]);
|
|
281
|
+
}
|
|
282
|
+
return { present: true, ids, valid: ids.length > 0 };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** 'always' | 'default' (absent, `true`, `${{ true }}` = implicit success()) | 'conditional'. */
|
|
286
|
+
function jobConditionKind(job) {
|
|
287
|
+
const { condition } = controlValues(job.body.split('\n'), jobPropertyIndent(job));
|
|
288
|
+
if (condition === undefined) return 'default';
|
|
289
|
+
if (SAFE_NEEDS_IF.test(condition)) return 'always';
|
|
290
|
+
return SAFE_IF.test(condition) ? 'default' : 'conditional';
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function jobIndex(jobs) {
|
|
294
|
+
return new Map(jobs.map((job) => [job.id, job]));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* True when nothing in the `needs:` chain can intentionally skip this job.
|
|
299
|
+
*
|
|
300
|
+
* GitHub skips a job whose needed job failed, and a skipped job satisfies a required
|
|
301
|
+
* status check. An unconditional chain is still fail-closed for the *workflow*: its
|
|
302
|
+
* only skip path is an upstream failure that fails the same run. Whether that upstream
|
|
303
|
+
* failure also blocks the merge is a provider question, answered by required-status
|
|
304
|
+
* correlation over `upstreamJobs`. A conditional `if:` anywhere in the chain (for
|
|
305
|
+
* example a CI-profile output) is an intentional skip reported as success, so the job
|
|
306
|
+
* cannot enforce.
|
|
307
|
+
*/
|
|
308
|
+
function needsChainUnconditional(job, byId, seen = new Set()) {
|
|
309
|
+
const needs = jobNeeds(job);
|
|
310
|
+
if (!needs.present) return true;
|
|
311
|
+
if (!needs.valid || seen.has(job.id)) return false;
|
|
312
|
+
const nextSeen = new Set(seen).add(job.id);
|
|
313
|
+
return needs.ids.every((id) => {
|
|
314
|
+
const upstream = byId.get(id);
|
|
315
|
+
if (!upstream || nextSeen.has(id)) return false;
|
|
316
|
+
const kind = jobConditionKind(upstream);
|
|
317
|
+
if (kind === 'conditional') return false;
|
|
318
|
+
return kind === 'always' || needsChainUnconditional(upstream, byId, nextSeen);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function jobCannotEnforce(job, byId) {
|
|
323
|
+
if (unsafeControl(job.body.split('\n'), jobPropertyIndent(job))) return true;
|
|
324
|
+
if (jobConditionKind(job) === 'always') return false;
|
|
325
|
+
return !needsChainUnconditional(job, byId);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Jobs whose failure skips `job`; stops at `if: always()` upstreams, which still run. */
|
|
329
|
+
function upstreamJobs(job, byId, out = new Map()) {
|
|
330
|
+
for (const id of jobNeeds(job).ids) {
|
|
331
|
+
const upstream = byId.get(id);
|
|
332
|
+
if (!upstream || out.has(id)) continue;
|
|
333
|
+
out.set(id, upstream);
|
|
334
|
+
if (jobConditionKind(upstream) !== 'always') upstreamJobs(upstream, byId, out);
|
|
335
|
+
}
|
|
336
|
+
return out;
|
|
254
337
|
}
|
|
255
338
|
|
|
256
339
|
function commandsRunArk(commands, script) {
|
|
@@ -261,9 +344,10 @@ function commandsEnforceArk(commands, script) {
|
|
|
261
344
|
return analyzeCommands(commands, script).some((entry) => entry.enforcing && FAIL_CLOSED.test(entry.text));
|
|
262
345
|
}
|
|
263
346
|
|
|
264
|
-
|
|
347
|
+
function activeWorkflowRunText(text) {
|
|
265
348
|
const jobs = workflowJobs(text);
|
|
266
|
-
|
|
349
|
+
const byId = jobIndex(jobs);
|
|
350
|
+
return jobs.filter((job) => !jobCannotEnforce(job, byId)).map((job) => workflowRunText(job.body)).join('\n');
|
|
267
351
|
}
|
|
268
352
|
|
|
269
353
|
export function runsArkCheck(text, script = '') {
|
|
@@ -322,12 +406,16 @@ function localArkContexts(root) {
|
|
|
322
406
|
const known = new Set();
|
|
323
407
|
const occurrences = new Map();
|
|
324
408
|
const arkOccurrences = new Map();
|
|
409
|
+
// context -> contexts whose failure would skip it (null = an upstream has no literal context)
|
|
410
|
+
const upstream = new Map();
|
|
325
411
|
let dynamic = false;
|
|
326
412
|
for (const workflow of workflowFiles(root)) {
|
|
327
|
-
|
|
413
|
+
const jobs = workflowJobs(workflow.text);
|
|
414
|
+
const byId = jobIndex(jobs);
|
|
415
|
+
for (const job of jobs) {
|
|
328
416
|
const explicit = jobName(job);
|
|
329
417
|
const matrix = jobHasMatrix(job);
|
|
330
|
-
const enforcing = !jobCannotEnforce(job) &&
|
|
418
|
+
const enforcing = !jobCannotEnforce(job, byId) &&
|
|
331
419
|
commandsEnforceArk(workflowRunText(job.body), script);
|
|
332
420
|
if (enforcing) {
|
|
333
421
|
known.add(job.id);
|
|
@@ -339,7 +427,15 @@ function localArkContexts(root) {
|
|
|
339
427
|
}
|
|
340
428
|
const context = explicit.name ?? job.id;
|
|
341
429
|
occurrences.set(context, (occurrences.get(context) ?? 0) + 1);
|
|
342
|
-
if (enforcing)
|
|
430
|
+
if (enforcing) {
|
|
431
|
+
arkOccurrences.set(context, (arkOccurrences.get(context) ?? 0) + 1);
|
|
432
|
+
const skippers = jobConditionKind(job) === 'always' ? [] : [...upstreamJobs(job, byId).values()];
|
|
433
|
+
const names = skippers.map((needed) => {
|
|
434
|
+
const neededName = jobName(needed);
|
|
435
|
+
return neededName.dynamic || jobHasMatrix(needed) ? null : neededName.name ?? needed.id;
|
|
436
|
+
});
|
|
437
|
+
upstream.set(context, names.includes(null) ? null : names);
|
|
438
|
+
}
|
|
343
439
|
}
|
|
344
440
|
}
|
|
345
441
|
const contexts = new Set();
|
|
@@ -347,7 +443,19 @@ function localArkContexts(root) {
|
|
|
347
443
|
if (count === 1 && occurrences.get(context) === 1) contexts.add(context);
|
|
348
444
|
else dynamic = true;
|
|
349
445
|
}
|
|
350
|
-
return { contexts, known, dynamic };
|
|
446
|
+
return { contexts, known, dynamic, upstream };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Required-status names that must also be required so an upstream failure (which skips
|
|
451
|
+
* the Ark job, and a skipped job satisfies branch protection) still blocks the merge.
|
|
452
|
+
* Returns [] when the chain is closed, otherwise the missing names ('<dynamic>' when an
|
|
453
|
+
* upstream only has a matrix/expression name that cannot be required exactly).
|
|
454
|
+
*/
|
|
455
|
+
function unrequiredUpstream(local, context, requiredNames) {
|
|
456
|
+
const names = local.upstream.get(context);
|
|
457
|
+
if (names === null) return ['<dynamic>'];
|
|
458
|
+
return (names ?? []).filter((name) => !requiredNames.has(name));
|
|
351
459
|
}
|
|
352
460
|
|
|
353
461
|
/** Historical name retained; the set also contains literal, non-dynamic job names. */
|
|
@@ -357,17 +465,31 @@ export function jobIdsThatRunArkCheck(root) {
|
|
|
357
465
|
|
|
358
466
|
export function isArkRequiredStatusCheck(root, requiredNames) {
|
|
359
467
|
if (!Array.isArray(requiredNames) || requiredNames.length === 0) return false;
|
|
360
|
-
const local = localArkContexts(root)
|
|
361
|
-
|
|
468
|
+
const local = localArkContexts(root);
|
|
469
|
+
const required = new Set(requiredNames.map(String));
|
|
470
|
+
return [...required].some((name) =>
|
|
471
|
+
local.contexts.has(name) && unrequiredUpstream(local, name, required).length === 0
|
|
472
|
+
);
|
|
362
473
|
}
|
|
363
474
|
|
|
364
|
-
|
|
365
|
-
|
|
475
|
+
/**
|
|
476
|
+
* `requiredNames` is every required context across classic protection and rulesets: an
|
|
477
|
+
* Ark context only counts as required when each job that can skip it is required too.
|
|
478
|
+
*/
|
|
479
|
+
function classifyRequired(local, legacyContexts, checks, bindingField, requiredNames) {
|
|
366
480
|
const exact = (name) => local.contexts.has(String(name));
|
|
481
|
+
const closed = (name) => unrequiredUpstream(local, String(name), requiredNames).length === 0;
|
|
367
482
|
const matching = checks.filter((check) => exact(check.context));
|
|
368
|
-
|
|
483
|
+
// Unbound (any source) or bound to the GitHub Actions app that runs the local workflow job.
|
|
484
|
+
const satisfiable = matching.filter((check) =>
|
|
485
|
+
check[bindingField] == null || check[bindingField] === -1 ||
|
|
486
|
+
check[bindingField] === GITHUB_ACTIONS_APP_ID
|
|
487
|
+
);
|
|
488
|
+
if (satisfiable.some((check) => closed(check.context))) return true;
|
|
369
489
|
if (matching.length > 0) return 'unverified';
|
|
370
|
-
|
|
490
|
+
const legacy = legacyContexts.filter(exact);
|
|
491
|
+
if (legacy.some(closed)) return true;
|
|
492
|
+
if (legacy.length > 0) return 'unverified';
|
|
371
493
|
return local.dynamic && (legacyContexts.length > 0 || checks.length > 0)
|
|
372
494
|
? 'unverified'
|
|
373
495
|
: false;
|
|
@@ -421,7 +543,7 @@ export function classifyGithubProviderFailure(errorText, opts = {}) {
|
|
|
421
543
|
* @param {{ name?: string, workflowName?: string, displayTitle?: string }} run
|
|
422
544
|
* @returns {boolean}
|
|
423
545
|
*/
|
|
424
|
-
|
|
546
|
+
function isArkishCiRun(run) {
|
|
425
547
|
const blob = `${run?.name || ''} ${run?.workflowName || ''} ${run?.displayTitle || ''}`.toLowerCase();
|
|
426
548
|
if (!blob.trim()) return false;
|
|
427
549
|
// Product tokens with word boundaries (avoid spark, dark, lark false positives).
|
|
@@ -558,18 +680,32 @@ export function reportGithubBranchProtection(opts = {}) {
|
|
|
558
680
|
})).filter((check) => check.context)
|
|
559
681
|
: [];
|
|
560
682
|
const hasWorkflowRule = rulesAvailable && rules.some((rule) => rule?.type === 'workflows');
|
|
683
|
+
const all = [...new Set([...contexts, ...checks.map((check) => check.context), ...statusRules.map((check) => check.context)])];
|
|
684
|
+
const requiredNames = new Set(all);
|
|
685
|
+
const local = localArkContexts(cwd);
|
|
561
686
|
const classicRequired = classicAvailable
|
|
562
|
-
? classifyRequired(
|
|
687
|
+
? classifyRequired(local, contexts, checks, 'app_id', requiredNames)
|
|
563
688
|
: 'unverified';
|
|
564
689
|
const rulesRequired = rulesAvailable
|
|
565
|
-
? classifyRequired(
|
|
690
|
+
? classifyRequired(local, [], statusRules, 'integration_id', requiredNames)
|
|
566
691
|
: 'unverified';
|
|
567
692
|
const arkCheckRequired = combineRequired(
|
|
568
693
|
classicRequired, rulesRequired, classicAvailable, rulesAvailable, hasWorkflowRule
|
|
569
694
|
);
|
|
570
|
-
|
|
695
|
+
// true only when every required Ark check is bound to GitHub Actions; unbound or
|
|
696
|
+
// legacy-context-only requirements can be satisfied by any status writer.
|
|
697
|
+
const arkBindings = [
|
|
698
|
+
...checks.map((check) => ({ context: check.context, id: check.app_id })),
|
|
699
|
+
...statusRules.map((check) => ({ context: check.context, id: check.integration_id })),
|
|
700
|
+
].filter((check) => local.contexts.has(check.context));
|
|
701
|
+
const arkCheckSourceBound = arkCheckRequired !== true
|
|
702
|
+
? 'unverified'
|
|
703
|
+
: arkBindings.length > 0 && arkBindings.every((check) => check.id === GITHUB_ACTIONS_APP_ID);
|
|
571
704
|
const available = arkCheckRequired === true || (classicAvailable && rulesAvailable);
|
|
572
|
-
|
|
705
|
+
// Required Ark contexts that an unrequired upstream failure could skip (reported as success).
|
|
706
|
+
const arkCheckUpstreamNotRequired = [...new Set(all
|
|
707
|
+
.filter((name) => local.contexts.has(name))
|
|
708
|
+
.flatMap((name) => unrequiredUpstream(local, name, requiredNames)))];
|
|
573
709
|
const error = `${classicResult.stderr || ''}${rulesResult.stderr || ''}`.slice(0, 400);
|
|
574
710
|
|
|
575
711
|
let reason = available ? 'ok' : 'provider-enforcement-unverified';
|
|
@@ -607,6 +743,7 @@ export function reportGithubBranchProtection(opts = {}) {
|
|
|
607
743
|
enforcesAdmins: classicAvailable ? Boolean(classic.enforcesAdmins) : null,
|
|
608
744
|
arkCheckRequired,
|
|
609
745
|
arkCheckSourceBound,
|
|
746
|
+
arkCheckUpstreamNotRequired,
|
|
610
747
|
// hard merge remains false when status is not proven required
|
|
611
748
|
hard: arkCheckRequired === true ? undefined : false,
|
|
612
749
|
runtimeObserved: ciRuntime.runtimeObserved === true,
|
|
@@ -10,7 +10,7 @@ import path from 'node:path';
|
|
|
10
10
|
/** Relative path from project root (stable contract). */
|
|
11
11
|
export const GOLDEN_PATTERN_REL = '.ark/golden-pattern.json';
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
const GOLDEN_PATTERN_SCHEMA_VERSION = '1';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* @typedef {{
|
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
// Generated from hook-templates.source.mjs — run npm run generate:packaged-tooling.
|
|
2
|
-
import{execCommandParts as
|
|
3
|
-
`}function
|
|
4
|
-
`}function
|
|
5
|
-
`}function
|
|
2
|
+
import{execCommandParts as h,execRunner as d}from"../ark-shared.mjs";const m="arkgate-mcp";function y(t,r){let o,e;try{o=t&&t.trim()?JSON.parse(t):{},e=JSON.parse(r)}catch{return null}return!o||typeof o!="object"||Array.isArray(o)||!e||typeof e!="object"||Array.isArray(e)?null:{existing:o,generated:e}}function x(t){const r=d(t);return`${JSON.stringify({version:1,hooks:{preToolUse:[{command:`${r} ${m} --hook --hook-repair --fail-on-new-smells --root . --root-env CURSOR_PROJECT_DIR --config ark.config.json`,matcher:"Write|StrReplace",failClosed:!0,timeout:30}]}},null,2)}
|
|
3
|
+
`}function _(t){const r=d(t);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",command:`${r} ${m} --session-context --root . --root-env CLAUDE_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit",hooks:[{type:"command",command:`${r} ${m} --hook --hook-repair --fail-on-new-smells --root . --root-env CLAUDE_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
|
|
4
|
+
`}function S(t){const r=d(t);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${m} --session-context --root . --root-env CODEX_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"ApplyPatch|apply_patch|Write|Edit|MultiEdit",hooks:[{type:"command",timeout:30,command:`${r} ${m} --hook --hook-repair --fail-on-new-smells --root . --root-env CODEX_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
|
|
5
|
+
`}function C(t){const{command:r,args:o}=h(t,m,["--root",".","--config","ark.config.json"]),e=n=>n.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),s=o.map(n=>`"${e(n)}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Codex project scope).
|
|
6
6
|
# CONFIGURED ON DISK \u2014 RUNTIME NOT VERIFIED.
|
|
7
7
|
# Restart Codex, then call ark_identity with expectedRoot before trusting MCP verdicts.
|
|
8
8
|
[mcp_servers.ark]
|
|
9
|
-
command = "${
|
|
10
|
-
args = [${
|
|
11
|
-
`}function
|
|
9
|
+
command = "${e(r)}"
|
|
10
|
+
args = [${s}]
|
|
11
|
+
`}function E(t){const{command:r,args:o}=h(t,m,["--root",".","--config","ark.config.json"]),e=o.map(s=>`"${s.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Grok Build project scope).
|
|
12
12
|
# Restart Grok (or /mcps \u2192 refresh) after changes. Also loads repo-root .mcp.json.
|
|
13
13
|
[mcp_servers.ark]
|
|
14
14
|
command = "${r.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"
|
|
15
|
-
args = [${
|
|
16
|
-
`}function
|
|
17
|
-
`}function
|
|
18
|
-
`}function
|
|
19
|
-
`}function
|
|
20
|
-
`}function
|
|
21
|
-
`}function
|
|
22
|
-
`}
|
|
15
|
+
args = [${e}]
|
|
16
|
+
`}function v(t){const r=d(t);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${m} --session-context --root . --root-env GROK_WORKSPACE_ROOT,CLAUDE_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit|write|search_replace",hooks:[{type:"command",timeout:30,command:`${r} ${m} --hook --hook-repair --fail-on-new-smells --root . --root-env GROK_WORKSPACE_ROOT,CLAUDE_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
|
|
17
|
+
`}function J(t){const r=d(t);return`${JSON.stringify({"ark-write-gate":{PreToolUse:[{matcher:"write_to_file|replace_file_content|multi_replace_file_content",hooks:[{type:"command",timeout:30,command:`${r} ${m} --hook --hook-repair --fail-on-new-smells --root . --config ark.config.json`}]}]}},null,2)}
|
|
18
|
+
`}function P(t){const{command:r,args:o}=h(t,m,["--root",".","--config","ark.config.json"]);return`${JSON.stringify({$schema:"https://opencode.ai/config.json",mcp:{ark:{type:"local",command:[r,...o],enabled:!0}}},null,2)}
|
|
19
|
+
`}function N(t,r){const o=y(t,r);if(!o)return null;const{existing:e,generated:s}=o,n=s.hooks,p=(Array.isArray(n?.preToolUse)?n.preToolUse:[]).find(c=>c&&typeof c=="object"&&typeof c.command=="string"&&/arkgate-mcp|ark-mcp/.test(c.command)&&/\s--hook(?:\s|$)/.test(` ${c.command} `));if(!p)return null;const i=e.hooks&&typeof e.hooks=="object"&&!Array.isArray(e.hooks)?{...e.hooks}:{},l=Array.isArray(i.preToolUse)?[...i.preToolUse]:[],u=c=>c&&typeof c=="object"&&typeof c.command=="string"&&/arkgate-mcp|ark-mcp/.test(c.command)&&/\s--hook(?:\s|$)/.test(` ${c.command} `)&&(typeof c.matcher!="string"||/Write/.test(c.matcher)||/StrReplace/.test(c.matcher)),a=l.filter(c=>!u(c));i.preToolUse=[...a,p];const f={...e,version:e.version??s.version??1,hooks:i};return`${JSON.stringify(f,null,2)}
|
|
20
|
+
`}const $=/(?:^|[\s/])(?:arkgate-mcp|ark-mcp)(?:\.mjs)?(?:\s|$)/,b=/\s--(?:hook|session-context)(?:\s|$)/;function j(t){return!!t&&typeof t=="object"&&typeof t.command=="string"&&$.test(` ${t.command} `)&&b.test(` ${t.command} `)}function T(t,r){const o=y(t,r);if(!o)return null;const{existing:e,generated:s}=o,n=s.hooks;if(!n||typeof n!="object"||e.hooks!==void 0&&(!e.hooks||typeof e.hooks!="object"||Array.isArray(e.hooks)))return null;const k={...e.hooks??{}};for(const[p,i]of Object.entries(n)){if(!Array.isArray(i))continue;const l=k[p];if(l!==void 0&&!Array.isArray(l))return null;const u=[];for(const a of l??[]){if(!a||typeof a!="object"||!Array.isArray(a.hooks)){u.push(a);continue}const f=a.hooks.filter(c=>!j(c));f.length===a.hooks.length?u.push(a):f.length>0&&u.push({...a,hooks:f})}k[p]=[...u,...i]}return`${JSON.stringify({...e,hooks:k},null,2)}
|
|
21
|
+
`}function g(t){const r=o=>typeof o=="string"?o.replace(/arkgate@[^\s"']+/g,"arkgate@*"):Array.isArray(o)?o.map(r):o&&typeof o=="object"?Object.fromEntries(Object.keys(o).sort().map(e=>[e,r(o[e])])):o;return JSON.stringify(r(t))}function A(t,r){try{const o=JSON.parse(t),e=JSON.parse(r);return!o||typeof o!="object"||Array.isArray(o)||!e||typeof e!="object"||Array.isArray(e)?null:{existing:o,generated:e}}catch{return null}}function O(t){return{status:"removed",text:Object.keys(t).length===0?null:`${JSON.stringify(t,null,2)}
|
|
22
|
+
`}}function U(t,r){const o=A(t,r);if(!o)return{status:"unreadable"};const{existing:e,generated:s}=o;if(e.hooks===void 0)return{status:"absent"};if(!e.hooks||typeof e.hooks!="object"||Array.isArray(e.hooks))return{status:"unreadable"};const n={...e.hooks};let k=0;for(const[i,l]of Object.entries(s.hooks??{})){if(!Array.isArray(l)||n[i]===void 0)continue;if(!Array.isArray(n[i]))return{status:"unreadable"};const u=new Set(l.map(g)),a=[];for(const f of n[i]){if(!(f&&typeof f=="object"&&Array.isArray(f.hooks)&&f.hooks.some(j))){a.push(f);continue}if(!u.has(g(f)))return{status:"customized"};k+=1}a.length>0?n[i]=a:delete n[i]}if(k===0)return{status:"absent"};const p={...e,hooks:n};return Object.keys(n).length===0&&delete p.hooks,O(p)}function D(t,r){const o=A(t,r);if(!o)return{status:"unreadable"};const{existing:e,generated:s}=o,n=e.hooks?.preToolUse;if(n===void 0)return{status:"absent"};if(!Array.isArray(n))return{status:"unreadable"};const k=new Set((Array.isArray(s.hooks?.preToolUse)?s.hooks.preToolUse:[]).map(g)),p=[];let i=0;for(const a of n){if(!(a&&typeof a=="object"&&typeof a.command=="string"&&/arkgate-mcp|ark-mcp/.test(a.command)&&/\s--hook(?:\s|$)/.test(` ${a.command} `))){p.push(a);continue}if(!k.has(g(a)))return{status:"customized"};i+=1}if(i===0)return{status:"absent"};const l={...e.hooks};p.length>0?l.preToolUse=p:delete l.preToolUse;const u={...e,hooks:l};return Object.keys(l).length===0&&delete u.hooks,Object.keys(u).length===1&&u.version===(s.version??1)&&delete u.version,O(u)}function H(t,r){const o=A(t,r);if(!o)return{status:"unreadable"};const{existing:e,generated:s}=o;if(!Object.prototype.hasOwnProperty.call(e,"ark-write-gate"))return{status:"absent"};if(g(e["ark-write-gate"])!==g(s["ark-write-gate"]))return{status:"customized"};const n={...e};return delete n["ark-write-gate"],O(n)}function w(t,r){const o=y(t,r);if(!o)return null;const{existing:e,generated:s}=o,n=s["ark-write-gate"];if(!n||typeof n!="object")return null;const k={...e,"ark-write-gate":n};return`${JSON.stringify(k,null,2)}
|
|
23
|
+
`}function I(t,r){let o,e;try{o=t&&t.trim()?JSON.parse(t):{},e=JSON.parse(r)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o))return null;const s={...o};!s.$schema&&e.$schema&&(s.$schema=e.$schema);const n=o.mcp&&typeof o.mcp=="object"&&!Array.isArray(o.mcp)?{...o.mcp}:{};return n.ark=e.mcp.ark,s.mcp=n,`${JSON.stringify(s,null,2)}
|
|
24
|
+
`}export{m as PREFERRED_MCP_BIN,J as antigravityHooks,_ as claudeSettings,S as codexHooks,C as codexProjectConfig,x as cursorHooks,v as grokHooks,E as grokProjectConfig,w as mergeAntigravityArkHook,T as mergeClaudeStyleArkHooks,N as mergeCursorArkHook,I as mergeOpencodeArkMcp,P as opencodeProjectConfig,y as parseJsonMergeInputs,H as removeAntigravityArkHook,U as removeClaudeStyleArkHooks,D as removeCursorArkHook};
|
|
@@ -37,7 +37,9 @@ function hostProfile(label, hookPath, hookSurface, hookOperations, hardWrite, re
|
|
|
37
37
|
}),
|
|
38
38
|
// Host-native "deny when the checker cannot run" flag (FS-permission analog).
|
|
39
39
|
// required = schema field must be true for hard-write evidence (Cursor failClosed).
|
|
40
|
-
// unsupported = host has no documented flag
|
|
40
|
+
// unsupported = host has no documented flag. ark-mcp --hook fails closed itself
|
|
41
|
+
// (exit 2 / host deny) when its checker cannot run for a governed source write;
|
|
42
|
+
// only a crash of node itself before the launcher runs stays host-owned.
|
|
41
43
|
// none = advisory host; no local write block.
|
|
42
44
|
nativeFailClosed: extras.nativeFailClosed ?? (hardWrite ? 'unsupported' : 'none'),
|
|
43
45
|
// EH07 minimum ops matrix (hard=false for soft hosts on every listed op).
|
|
@@ -14,6 +14,8 @@ import { formatArkRunHtml } from './ark-run-report.mjs';
|
|
|
14
14
|
import { formatArkOrderHtml } from './ark-order-report.mjs';
|
|
15
15
|
import { primaryImprovementCompassNextAction } from './improvement-compass.mjs';
|
|
16
16
|
import { flatParentPilotHtml } from './flat-parent-pilot.mjs';
|
|
17
|
+
import { orphanModulesHtml } from './orphan-modules-io.mjs';
|
|
18
|
+
import { crossWallDuplicationHtml } from './duplication-io.mjs';
|
|
17
19
|
|
|
18
20
|
// htmlEscape is injected by the caller (html-report.mjs) — importing it back
|
|
19
21
|
// would be a dependency cycle, and the repo's own gate blocks that. The
|
|
@@ -362,6 +364,8 @@ export function renderAdvisorySections(advisories, escape) {
|
|
|
362
364
|
physicalCohesionHtml(advisories.physicalCohesion),
|
|
363
365
|
parseHealthHtml(advisories.parseHealth),
|
|
364
366
|
graphBlindSpotsHtml(advisories.graphBlindSpots, esc),
|
|
367
|
+
orphanModulesHtml(advisories.orphanModules, esc),
|
|
368
|
+
crossWallDuplicationHtml(advisories.crossWallDuplication, esc),
|
|
365
369
|
rulesUnderContractHtml(advisories.rulesUnderContract),
|
|
366
370
|
formatArkRunHtml(advisories.arkRun, esc),
|
|
367
371
|
formatArkOrderHtml(advisories.arkOrder, esc),
|
|
@@ -28,7 +28,7 @@ import { buildDeepModuleCoachAdvisory } from './deep-module-coach.mjs';
|
|
|
28
28
|
import { computePhysicalCohesion } from './physical-cohesion.mjs';
|
|
29
29
|
import { computeReshapeDecisionMemory } from './reshape-decisions.mjs';
|
|
30
30
|
import { POST_GREEN_LEDE, operatingModeTitle } from './product-copy.mjs';
|
|
31
|
-
import { anyChildWallAdvisory,
|
|
31
|
+
import { anyChildWallAdvisory, sliceAliasOwesMove } from '../ark-layer-match.mjs';
|
|
32
32
|
|
|
33
33
|
function esc(value) {
|
|
34
34
|
return String(value)
|
|
@@ -176,7 +176,7 @@ export function buildReportDepthPayload(
|
|
|
176
176
|
primaryNextAction: postGreenPath?.action ?? dualTruthNext,
|
|
177
177
|
activeBlockingViolations: activeBlockingCount,
|
|
178
178
|
physicalCohesionResidual: physicalCohesionResidualRemains(physicalCohesion), childWallAdvisory: anyChildWallAdvisory(config?.rules),
|
|
179
|
-
sliceAliasDebt:
|
|
179
|
+
sliceAliasDebt: sliceAliasOwesMove(config?.rules),
|
|
180
180
|
});
|
|
181
181
|
// Doctor parity: same physical-cohesion + baseline stale facts as runDoctor.
|
|
182
182
|
const baselineStale =
|
package/bin/lib/html-report.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
resolveOperatingMode,
|
|
12
12
|
} from '../ark-shared.mjs';
|
|
13
13
|
import { collectAdoptionGaps, arkCheckCommand } from './agent-gates.mjs';
|
|
14
|
-
import {
|
|
14
|
+
import { LEFTOVER_DESIGN_LABEL } from './product-copy.mjs';
|
|
15
15
|
import { CORE_LAYER_NAMES } from './core-layers.mjs';
|
|
16
16
|
import {
|
|
17
17
|
renderBaselineSignalLegend,
|
|
@@ -572,7 +572,7 @@ export function renderHtmlReport({
|
|
|
572
572
|
const designSmells = Array.isArray(depth.designSmells) ? depth.designSmells : [];
|
|
573
573
|
const designWeakBadge =
|
|
574
574
|
designFitness?.designWeak === true
|
|
575
|
-
? ` <span class="badge design" title="Import rules can be green while leftover design work remains. Not a FAIL.">${
|
|
575
|
+
? ` <span class="badge design" title="Import rules can be green while leftover design work remains. Not a FAIL.">${LEFTOVER_DESIGN_LABEL}</span>`
|
|
576
576
|
: '';
|
|
577
577
|
const designStripHtml =
|
|
578
578
|
renderDesignDepthStrip({
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact importer index for doctor and report (ADR 0037 D1).
|
|
3
|
+
*
|
|
4
|
+
* The scan releases the resolved facts right after the verdict (memory). This
|
|
5
|
+
* keeps only what the files-nothing-imports advisory and the flat-parent
|
|
6
|
+
* suggestion read: governed paths, incoming edge counts, the edge list as
|
|
7
|
+
* typed-array pairs, and which export names each target is imported by. It is
|
|
8
|
+
* a projection of facts that already exist — never an input to the verdict.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Unresolved project-looking specifiers kept for `maybe-unresolved` matching. */
|
|
12
|
+
const MAX_UNRESOLVED_SPECIFIERS = 200;
|
|
13
|
+
/** A bare specifier that looks like a project alias rather than a package. */
|
|
14
|
+
const ALIAS_LIKE = /^(?:[@~#$]\/|[~#$])/;
|
|
15
|
+
|
|
16
|
+
function looksLikeProjectSpecifier(specifier) {
|
|
17
|
+
return typeof specifier === 'string' && (specifier.startsWith('.') || ALIAS_LIKE.test(specifier));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {{ files?: Array<{ path: string, parseStatus?: string }>, dependencies?: Array<object>, completeness?: string }} facts
|
|
22
|
+
* @returns {{
|
|
23
|
+
* files: string[],
|
|
24
|
+
* importerCount: Uint32Array,
|
|
25
|
+
* edges: Int32Array,
|
|
26
|
+
* namedUse: Map<number, Set<string> | '*'>,
|
|
27
|
+
* unresolved: Array<{ from: string, specifier: string }>,
|
|
28
|
+
* unresolvedTotal: number,
|
|
29
|
+
* dynamicSites: number,
|
|
30
|
+
* invalidFiles: number,
|
|
31
|
+
* completeness: string,
|
|
32
|
+
* }}
|
|
33
|
+
*/
|
|
34
|
+
export function projectImporterIndex(facts) {
|
|
35
|
+
const files = [];
|
|
36
|
+
const indexOf = new Map();
|
|
37
|
+
let invalidFiles = 0;
|
|
38
|
+
for (const file of Array.isArray(facts?.files) ? facts.files : []) {
|
|
39
|
+
if (typeof file?.path !== 'string' || indexOf.has(file.path)) continue;
|
|
40
|
+
indexOf.set(file.path, files.length);
|
|
41
|
+
files.push(file.path);
|
|
42
|
+
if (file.parseStatus === 'invalid') invalidFiles += 1;
|
|
43
|
+
}
|
|
44
|
+
const importerCount = new Uint32Array(files.length);
|
|
45
|
+
const pairs = [];
|
|
46
|
+
const namedUse = new Map();
|
|
47
|
+
const unresolved = [];
|
|
48
|
+
let unresolvedTotal = 0;
|
|
49
|
+
let dynamicSites = 0;
|
|
50
|
+
for (const dep of Array.isArray(facts?.dependencies) ? facts.dependencies : []) {
|
|
51
|
+
if (dep?.resolution === 'dynamic') {
|
|
52
|
+
dynamicSites += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (dep?.resolution === 'unresolved') {
|
|
56
|
+
if (!looksLikeProjectSpecifier(dep.specifier)) continue;
|
|
57
|
+
unresolvedTotal += 1;
|
|
58
|
+
if (unresolved.length < MAX_UNRESOLVED_SPECIFIERS) {
|
|
59
|
+
unresolved.push({ from: dep.from, specifier: dep.specifier });
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (dep?.resolution !== 'resolved-project') continue;
|
|
64
|
+
const from = indexOf.get(dep.from);
|
|
65
|
+
const to = indexOf.get(dep.target);
|
|
66
|
+
if (from === undefined || to === undefined || from === to) continue;
|
|
67
|
+
pairs.push(from, to);
|
|
68
|
+
importerCount[to] += 1;
|
|
69
|
+
const previous = namedUse.get(to);
|
|
70
|
+
if (previous === '*') continue;
|
|
71
|
+
// Default, namespace, side-effect, dynamic, require and `export *` carry no
|
|
72
|
+
// names: any export may be used through them.
|
|
73
|
+
const named = Array.isArray(dep.namedBindings) && dep.namedBindings.length > 0 ? dep.namedBindings : null;
|
|
74
|
+
if (!named) {
|
|
75
|
+
namedUse.set(to, '*');
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const set = previous ?? new Set();
|
|
79
|
+
for (const name of named) set.add(name);
|
|
80
|
+
namedUse.set(to, set);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
files,
|
|
84
|
+
importerCount,
|
|
85
|
+
edges: Int32Array.from(pairs),
|
|
86
|
+
namedUse,
|
|
87
|
+
unresolved,
|
|
88
|
+
unresolvedTotal,
|
|
89
|
+
dynamicSites,
|
|
90
|
+
invalidFiles,
|
|
91
|
+
completeness: typeof facts?.completeness === 'string' ? facts.completeness : 'unavailable',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* target path → every governed file that imports it (the flat-parent graph).
|
|
97
|
+
* @param {ReturnType<typeof projectImporterIndex>} index
|
|
98
|
+
*/
|
|
99
|
+
export function importerGraphFromIndex(index) {
|
|
100
|
+
const graph = new Map();
|
|
101
|
+
const { files, edges } = index;
|
|
102
|
+
for (let i = 0; i + 1 < edges.length; i += 2) {
|
|
103
|
+
const target = files[edges[i + 1]];
|
|
104
|
+
let set = graph.get(target);
|
|
105
|
+
if (!set) {
|
|
106
|
+
set = new Set();
|
|
107
|
+
graph.set(target, set);
|
|
108
|
+
}
|
|
109
|
+
set.add(files[edges[i]]);
|
|
110
|
+
}
|
|
111
|
+
return graph;
|
|
112
|
+
}
|
|
@@ -56,7 +56,19 @@ export function readTsconfigAliases(ts, root) {
|
|
|
56
56
|
if (!configPath) return { baseUrl: root, aliases: [] };
|
|
57
57
|
const read = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
58
58
|
if (read.error) return { baseUrl: root, aliases: [] };
|
|
59
|
-
|
|
59
|
+
// Options-only parse: `fileNames` are unused here. With the real `ts.sys` the
|
|
60
|
+
// parser walked the whole `include` tree on every write-hook call (#205 twin).
|
|
61
|
+
const optionsOnlyHost = {
|
|
62
|
+
useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
|
|
63
|
+
readDirectory: () => [],
|
|
64
|
+
fileExists: (fileName) => ts.sys.fileExists(fileName),
|
|
65
|
+
readFile: (fileName) => ts.sys.readFile(fileName),
|
|
66
|
+
};
|
|
67
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
68
|
+
read.config,
|
|
69
|
+
optionsOnlyHost,
|
|
70
|
+
path.dirname(configPath)
|
|
71
|
+
);
|
|
60
72
|
const opts = parsed.options || {};
|
|
61
73
|
const baseUrl = opts.baseUrl || path.dirname(configPath);
|
|
62
74
|
const aliases = [];
|
|
@@ -2,11 +2,7 @@
|
|
|
2
2
|
* Doctor adapter for the Domain improvement compass (notAScore projection).
|
|
3
3
|
* Keeps doctor-plan.mjs inside its module budget; pure assembly only.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
buildImprovementCompass,
|
|
7
|
-
formatImprovementCompassDoctorLines,
|
|
8
|
-
primaryImprovementCompassNextAction,
|
|
9
|
-
} from './improvement-compass.mjs';
|
|
5
|
+
import { buildImprovementCompass } from './improvement-compass.mjs';
|
|
10
6
|
|
|
11
7
|
/**
|
|
12
8
|
* @param {{
|
|
@@ -92,22 +88,3 @@ export function buildDoctorImprovementCompass(input = {}) {
|
|
|
92
88
|
});
|
|
93
89
|
}
|
|
94
90
|
|
|
95
|
-
export {
|
|
96
|
-
formatImprovementCompassDoctorLines,
|
|
97
|
-
primaryImprovementCompassNextAction,
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Human doctor section (never a score bar).
|
|
102
|
-
* @param {import('./improvement-compass.mjs').ImprovementCompass} compass
|
|
103
|
-
* @param {{ line: Function, warn: string, ok: string, color: { bold: Function } }} io
|
|
104
|
-
*/
|
|
105
|
-
export function printImprovementCompassSection(compass, io) {
|
|
106
|
-
const { line, warn, ok, color } = io;
|
|
107
|
-
console.log('');
|
|
108
|
-
console.log(color.bold('Improvement compass (not a score)'));
|
|
109
|
-
const mark = compass.topResidual.length > 0 ? warn : ok;
|
|
110
|
-
for (const text of formatImprovementCompassDoctorLines(compass)) {
|
|
111
|
-
line(mark, text);
|
|
112
|
-
}
|
|
113
|
-
}
|