arkgate 4.8.24 → 4.8.26
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 +95 -0
- package/README.md +2 -2
- package/bin/ark-check-runtime.mjs +17 -14
- package/bin/ark-layer-match.mjs +115 -13
- package/bin/ark-mcp-runtime.mjs +36 -31
- package/bin/ark-shared.mjs +4 -23
- 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 +4 -4
- package/bin/lib/ambient-state.mjs +9 -30
- package/bin/lib/analysis-engine.mjs +14 -14
- package/bin/lib/architecture-scan.mjs +11 -9
- package/bin/lib/ark-order-doctor.mjs +3 -28
- package/bin/lib/ark-order-facts.mjs +6 -99
- package/bin/lib/ark-order-types.mjs +1 -2
- package/bin/lib/ark-run-doctor.mjs +3 -28
- package/bin/lib/ark-run-facts.mjs +30 -40
- package/bin/lib/ark-run-sensors.mjs +1 -1
- package/bin/lib/arkrule-file-hints.mjs +2 -47
- package/bin/lib/arkrules-contract.mjs +124 -133
- package/bin/lib/arkrules-sensors.mjs +7 -5
- package/bin/lib/check-args.mjs +63 -1
- package/bin/lib/ci-and-commands.mjs +8 -72
- package/bin/lib/ci-merge-boundary.mjs +1 -1
- package/bin/lib/class-source-scan.mjs +5 -5
- package/bin/lib/clone-detection.mjs +474 -0
- package/bin/lib/codex-home.mjs +1 -1
- package/bin/lib/config-contract-slices.mjs +44 -6
- package/bin/lib/config-contract.mjs +40 -117
- package/bin/lib/config-extras.mjs +16 -22
- package/bin/lib/contract-smells.mjs +33 -15
- package/bin/lib/core-ratchet.mjs +0 -2
- package/bin/lib/dashboard-cli.mjs +2 -2
- package/bin/lib/deep-module-coach.mjs +2 -4
- package/bin/lib/deploy-path.mjs +13 -38
- package/bin/lib/design-smells.mjs +2 -2
- package/bin/lib/diagnostic-catalog.mjs +8 -1
- package/bin/lib/doctor-advisories.mjs +55 -9
- package/bin/lib/doctor-green-cite.mjs +1 -1
- package/bin/lib/doctor-human.mjs +1 -1
- package/bin/lib/doctor-next-actions.mjs +2 -2
- package/bin/lib/doctor-plan.mjs +4 -6
- package/bin/lib/duplication-io.mjs +572 -0
- package/bin/lib/effective-contract-load.mjs +181 -44
- 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 +7 -3
- package/bin/lib/flat-parent-importers.mjs +5 -3
- package/bin/lib/gate-files.mjs +30 -89
- package/bin/lib/github-enforcement.mjs +165 -28
- package/bin/lib/golden-pattern.mjs +1 -1
- package/bin/lib/hook-templates.mjs +16 -16
- 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/improvement-compass-doctor.mjs +1 -24
- package/bin/lib/invariant-coverage-io.mjs +4 -3
- package/bin/lib/invariant-coverage.mjs +26 -0
- 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 +4 -5
- package/bin/lib/mcp-hook-payload.mjs +4 -4
- 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 +3 -3
- package/bin/lib/parse-health.mjs +1 -1
- package/bin/lib/persistence-write-hint.mjs +33 -8
- package/bin/lib/pin-dependent-gates.mjs +2 -2
- package/bin/lib/policy-delta-io.mjs +60 -12
- package/bin/lib/post-green-path.mjs +1 -1
- package/bin/lib/prepare-change.mjs +3 -9
- package/bin/lib/prepare-write.mjs +1 -2
- package/bin/lib/presets.mjs +56 -68
- package/bin/lib/product-copy.mjs +1 -6
- package/bin/lib/project-root.mjs +43 -85
- package/bin/lib/resolved-candidate-facts.mjs +90 -62
- package/bin/lib/rules-under-contract.mjs +72 -8
- package/bin/lib/scan-files.mjs +4 -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 +1 -1
- package/bin/lib/start-install-recovery.mjs +1 -1
- package/bin/lib/start-preview.mjs +1 -1
- package/bin/lib/states-transitions-presence.mjs +3 -3
- package/bin/lib/status-command.mjs +2 -18
- package/bin/lib/status-manifest.mjs +18 -26
- package/bin/lib/status-transition-catalog.mjs +0 -6
- package/bin/lib/suggestions.mjs +3 -3
- package/bin/lib/team-parliament-io.mjs +3 -17
- package/bin/lib/team-parliament.mjs +15 -9
- 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 +2 -2
- package/dist/chunk-G3JRBJGH.js +16 -0
- package/dist/chunk-R3AJ5RSZ.cjs +16 -0
- package/dist/{configTypes-BJbaeoj4.d.cts → configTypes-GPvvdleM.d.cts} +12 -1
- package/dist/{configTypes-BJbaeoj4.d.ts → configTypes-GPvvdleM.d.ts} +12 -1
- package/dist/eslint/index.cjs +11 -11
- package/dist/eslint/index.d.cts +10 -1
- package/dist/eslint/index.d.ts +10 -1
- package/dist/eslint/index.js +11 -11
- package/dist/index.cjs +34 -34
- package/dist/index.d.cts +587 -515
- package/dist/index.d.ts +587 -515
- package/dist/index.js +34 -34
- package/dist/nestjs/index.cjs +1 -1
- package/dist/nestjs/index.d.cts +1 -1
- package/dist/nestjs/index.d.ts +1 -1
- package/dist/nestjs/index.js +1 -1
- package/dist/order/index.cjs +1 -1
- package/dist/order/index.js +1 -1
- package/dist/runtime/index.cjs +12 -12
- package/dist/runtime/index.d.cts +174 -136
- package/dist/runtime/index.d.ts +174 -136
- package/dist/runtime/index.js +12 -12
- package/dist/{types-DRinG-vT.d.cts → types-CnauMQQL.d.cts} +33 -25
- package/dist/{types-DRinG-vT.d.ts → types-CnauMQQL.d.ts} +33 -25
- package/docs/README.md +3 -1
- package/docs/agent-guide.md +140 -6
- package/docs/ai-gates.md +15 -1
- package/docs/configuration.md +40 -11
- package/docs/diagnostics.md +119 -3
- package/docs/package-surface.md +4 -1
- package/docs/product-voice.md +7 -1
- package/package.json +11 -2
- package/schemas/ark.config.schema.json +31 -3
- package/schemas/ark.invariant-probe.schema.json +368 -0
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +1 -0
- 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-place/SKILL.md +8 -0
- package/templates/skills/ark-adopt.md +1 -0
- 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-place.md +8 -0
- package/bin/lib/ark-order-error.mjs +0 -18
- package/bin/lib/ark-order-invariants.mjs +0 -388
- package/bin/lib/stable-hash.mjs +0 -90
- package/dist/chunk-MBXCFLO4.js +0 -16
- package/dist/chunk-RNQHRDCQ.cjs +0 -16
package/bin/lib/gate-files.mjs
CHANGED
|
@@ -7,10 +7,10 @@ import { createRequire } from 'node:module';
|
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { codexProjectMcpIsValid } from './codex-home.mjs';
|
|
9
9
|
import { enforcingArkRunText, runsArkCheck } from './github-enforcement.mjs';
|
|
10
|
+
import { parseJsonMergeInputs } from './hook-templates.mjs';
|
|
10
11
|
import { npxArkgatePrefixLength } from './package-manager.mjs';
|
|
11
12
|
|
|
12
13
|
export const __packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
13
|
-
export const __arkCheckCli = path.join(__packageRoot, 'bin', 'ark-check.mjs');
|
|
14
14
|
|
|
15
15
|
export function readJson(file) {
|
|
16
16
|
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
@@ -43,13 +43,16 @@ export function packageScriptsHaveTypecheck(scripts) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* Visit the parsed package.json of each shallow nested package — `root/<dir>/`
|
|
47
|
+
* and `root/<dir>/<child>/`, skipping dot directories and `node_modules` — in
|
|
48
|
+
* directory order. Unreadable or invalid files are skipped. Stops at the first
|
|
49
|
+
* visit that returns true.
|
|
50
|
+
*
|
|
48
51
|
* @param {string} root
|
|
52
|
+
* @param {(pkg: any, dir: string) => boolean | void} visit
|
|
53
|
+
* @returns {boolean} true when a visit returned true
|
|
49
54
|
*/
|
|
50
|
-
export function
|
|
51
|
-
const pkg = readPackageJson(root);
|
|
52
|
-
if (packageScriptsHaveTypecheck(pkg?.scripts)) return true;
|
|
55
|
+
export function someNestedPackageJson(root, visit) {
|
|
53
56
|
try {
|
|
54
57
|
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
55
58
|
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
@@ -67,8 +70,7 @@ export function treeHasTypecheckScript(root) {
|
|
|
67
70
|
const pj = path.join(dir, 'package.json');
|
|
68
71
|
if (!fs.existsSync(pj)) continue;
|
|
69
72
|
try {
|
|
70
|
-
|
|
71
|
-
if (packageScriptsHaveTypecheck(nested.scripts)) return true;
|
|
73
|
+
if (visit(JSON.parse(fs.readFileSync(pj, 'utf8')), dir) === true) return true;
|
|
72
74
|
} catch {
|
|
73
75
|
/* ignore */
|
|
74
76
|
}
|
|
@@ -80,6 +82,17 @@ export function treeHasTypecheckScript(root) {
|
|
|
80
82
|
return false;
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Root package (and shallow nested packages) already have a typecheck script.
|
|
87
|
+
* Does not scan CI or framework configs — only package.json scripts.
|
|
88
|
+
* @param {string} root
|
|
89
|
+
*/
|
|
90
|
+
export function treeHasTypecheckScript(root) {
|
|
91
|
+
const pkg = readPackageJson(root);
|
|
92
|
+
if (packageScriptsHaveTypecheck(pkg?.scripts)) return true;
|
|
93
|
+
return someNestedPackageJson(root, (nested) => packageScriptsHaveTypecheck(nested.scripts));
|
|
94
|
+
}
|
|
95
|
+
|
|
83
96
|
/**
|
|
84
97
|
* Add a conservative `typecheck` script when the host has a TS/JS project config
|
|
85
98
|
* but no typecheck-like script yet. Never overwrites an existing script.
|
|
@@ -96,7 +109,7 @@ export function treeHasTypecheckScript(root) {
|
|
|
96
109
|
* True when `typescript` is declared by the project (deps/devDeps/peerDeps) or resolves
|
|
97
110
|
* from its root (e.g. hoisted in a workspace), so a generated `tsc --noEmit` can run.
|
|
98
111
|
*/
|
|
99
|
-
|
|
112
|
+
function projectHasTypeScript(root, pkg = readPackageJson(root) || {}) {
|
|
100
113
|
for (const field of ['dependencies', 'devDependencies', 'peerDependencies']) {
|
|
101
114
|
const deps = pkg?.[field];
|
|
102
115
|
if (deps && typeof deps === 'object' && Object.hasOwn(deps, 'typescript')) return true;
|
|
@@ -142,7 +155,7 @@ export const REQUIRED_GATE_FILES = [
|
|
|
142
155
|
'.mcp.json',
|
|
143
156
|
];
|
|
144
157
|
export const REQUIRED_GATE_WORKFLOW = '.github/workflows/*.yml running ark-check';
|
|
145
|
-
|
|
158
|
+
const CI_NOT_FAIL_CLOSED_ERROR = 'ci-not-fail-closed';
|
|
146
159
|
const COMPACT_ROUTER = /<!--\s*arkgate:compact-router host=([a-z]+)\s*-->/;
|
|
147
160
|
const FAIL_CLOSED_ARK_FLAG = /(?:^|\s)--(?:strict|strict-merge|require-gates)(?=\s|$)/;
|
|
148
161
|
|
|
@@ -317,16 +330,9 @@ export const CLAUDE_STYLE_HOOK_FILES = ['.claude/settings.json', '.codex/hooks.j
|
|
|
317
330
|
* Preserves sibling servers and unknown top-level keys. Returns null if unreadable.
|
|
318
331
|
*/
|
|
319
332
|
export function mergeArkMcpJson(existingText, generatedText) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
existing = existingText && existingText.trim() ? JSON.parse(existingText) : {};
|
|
324
|
-
generated = JSON.parse(generatedText);
|
|
325
|
-
} catch {
|
|
326
|
-
return null;
|
|
327
|
-
}
|
|
328
|
-
if (!existing || typeof existing !== 'object' || Array.isArray(existing)) return null;
|
|
329
|
-
if (!generated || typeof generated !== 'object' || Array.isArray(generated)) return null;
|
|
333
|
+
const parsed = parseJsonMergeInputs(existingText, generatedText);
|
|
334
|
+
if (!parsed) return null;
|
|
335
|
+
const { existing, generated } = parsed;
|
|
330
336
|
const generatedArk = generated.mcpServers?.ark;
|
|
331
337
|
if (!generatedArk || typeof generatedArk !== 'object' || Array.isArray(generatedArk)) {
|
|
332
338
|
return null;
|
|
@@ -604,71 +610,6 @@ function unquoteYamlScalar(value) {
|
|
|
604
610
|
return match ? match[2].trim() : text;
|
|
605
611
|
}
|
|
606
612
|
|
|
607
|
-
function jobCondition(lines, job) {
|
|
608
|
-
const condition = jobProperty(lines, job, 'if');
|
|
609
|
-
if (!condition) return 'default';
|
|
610
|
-
const value = unquoteYamlScalar(condition.value);
|
|
611
|
-
if (/^(?:\$\{\{\s*)?always\(\)(?:\s*\}\})?$/i.test(value)) return 'always';
|
|
612
|
-
if (/^(?:true|\$\{\{\s*true\s*\}\})$/i.test(value)) return 'true';
|
|
613
|
-
return 'conditional';
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
function jobNeeds(lines, job) {
|
|
617
|
-
const property = jobProperty(lines, job, 'needs');
|
|
618
|
-
if (!property) return { ids: [], indexes: [], valid: true };
|
|
619
|
-
const indexes = [property.index];
|
|
620
|
-
if (property.value) {
|
|
621
|
-
const value = unquoteYamlScalar(property.value);
|
|
622
|
-
const raw = value.startsWith('[') && value.endsWith(']')
|
|
623
|
-
? value.slice(1, -1).split(',')
|
|
624
|
-
: [value];
|
|
625
|
-
const ids = raw.map(unquoteYamlScalar).filter((id) => /^[A-Za-z0-9_-]+$/.test(id));
|
|
626
|
-
return { ids, indexes, valid: ids.length === raw.length && ids.length > 0 };
|
|
627
|
-
}
|
|
628
|
-
const ids = [];
|
|
629
|
-
for (let index = property.index + 1; index < job.end; index += 1) {
|
|
630
|
-
if (!lines[index].trim() || /^\s*#/.test(lines[index])) {
|
|
631
|
-
indexes.push(index);
|
|
632
|
-
continue;
|
|
633
|
-
}
|
|
634
|
-
const indent = lines[index].match(/^\s*/)?.[0].length ?? 0;
|
|
635
|
-
if (indent <= job.propertyIndent) break;
|
|
636
|
-
indexes.push(index);
|
|
637
|
-
const item = lines[index].match(/^\s*-\s*(['"]?)([A-Za-z0-9_-]+)\1\s*(?:#.*)?$/);
|
|
638
|
-
if (!item) return { ids: [], indexes, valid: false };
|
|
639
|
-
ids.push(item[2]);
|
|
640
|
-
}
|
|
641
|
-
return { ids, indexes, valid: ids.length > 0 };
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
function withVerifiedDependencyJobs(content) {
|
|
645
|
-
const { lines, jobs } = workflowJobSections(content);
|
|
646
|
-
const byId = new Map(jobs.map((job) => [job.id, job]));
|
|
647
|
-
const guaranteed = (job, seen = new Set()) => {
|
|
648
|
-
if (!job || seen.has(job.id)) return false;
|
|
649
|
-
const condition = jobCondition(lines, job);
|
|
650
|
-
if (condition === 'conditional') return false;
|
|
651
|
-
if (condition === 'always') return true;
|
|
652
|
-
const needs = jobNeeds(lines, job);
|
|
653
|
-
if (!needs.valid) return false;
|
|
654
|
-
const nextSeen = new Set(seen).add(job.id);
|
|
655
|
-
return needs.ids.every((id) => guaranteed(byId.get(id), nextSeen));
|
|
656
|
-
};
|
|
657
|
-
for (const job of jobs) {
|
|
658
|
-
const needs = jobNeeds(lines, job);
|
|
659
|
-
if (
|
|
660
|
-
needs.valid &&
|
|
661
|
-
needs.ids.length > 0 &&
|
|
662
|
-
needs.ids.every((id) => guaranteed(byId.get(id)))
|
|
663
|
-
) {
|
|
664
|
-
// The shared analyzer treats every `needs` as skippable. Hide it only after
|
|
665
|
-
// this dependency chain is proven unconditional; keep uncertain/skipped needs visible.
|
|
666
|
-
for (const index of needs.indexes) lines[index] = '';
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
return lines.join('\n');
|
|
670
|
-
}
|
|
671
|
-
|
|
672
613
|
function isGuaranteedJobCondition(value) {
|
|
673
614
|
const text = unquoteYamlScalar(value);
|
|
674
615
|
return (
|
|
@@ -732,13 +673,13 @@ export function inspectArkCiGate(root) {
|
|
|
732
673
|
} catch {
|
|
733
674
|
continue;
|
|
734
675
|
}
|
|
735
|
-
|
|
676
|
+
// The shared analyzer proves `needs:` chains (github-enforcement.mjs), so this
|
|
677
|
+
// file-level view and doctor merge-gate evidence cannot disagree.
|
|
678
|
+
const prepared = withFailClosedArkActions(content);
|
|
736
679
|
const failClosed = FAIL_CLOSED_ARK_FLAG.test(
|
|
737
680
|
enforcingArkRunText(prepared, failClosedScript)
|
|
738
681
|
);
|
|
739
|
-
const visible =
|
|
740
|
-
neutralizeSkippableJobControls(withFailClosedArkActions(content))
|
|
741
|
-
);
|
|
682
|
+
const visible = neutralizeSkippableJobControls(prepared);
|
|
742
683
|
const present =
|
|
743
684
|
failClosed ||
|
|
744
685
|
runsArkCheck(visible, declaredScript) ||
|
|
@@ -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,8 +21,8 @@ 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
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|$))/;
|
|
@@ -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,24 +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 C(
|
|
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 = "${e(
|
|
9
|
+
command = "${e(r)}"
|
|
10
10
|
args = [${s}]
|
|
11
|
-
`}function E(
|
|
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
|
-
command = "${
|
|
14
|
+
command = "${r.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"
|
|
15
15
|
args = [${e}]
|
|
16
|
-
`}function v(
|
|
17
|
-
`}function J(
|
|
18
|
-
`}function
|
|
19
|
-
`}function
|
|
20
|
-
`}const
|
|
21
|
-
`}function
|
|
22
|
-
`}}function U(r
|
|
23
|
-
`}function
|
|
24
|
-
`}export{
|
|
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};
|
|
@@ -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 =
|