arkgate 4.8.13 → 4.8.14
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 +86 -4
- package/README.md +23 -20
- package/SECURITY.md +5 -3
- package/bin/ark-check-runtime.mjs +9 -9
- package/bin/lib/agent-projection-formatters.mjs +2 -0
- package/bin/lib/agent-skills-package.mjs +63 -8
- package/bin/lib/analysis-engine.mjs +1 -1
- package/bin/lib/architecture-scan.mjs +8 -2
- package/bin/lib/ark-order-doctor.mjs +7 -1
- package/bin/lib/ark-order-report.mjs +2 -1
- package/bin/lib/ci-and-commands.mjs +7 -2
- package/bin/lib/design-smells.mjs +21 -1
- package/bin/lib/diagnostic-catalog.mjs +2 -2
- package/bin/lib/doctor-advisories.mjs +15 -7
- package/bin/lib/doctor-human.mjs +2 -1
- package/bin/lib/first-run-help.mjs +11 -2
- package/bin/lib/gate-files.mjs +40 -3
- package/bin/lib/install-migrate.mjs +23 -0
- package/bin/lib/mcp-hook-payload.mjs +1 -1
- package/bin/lib/product-copy.mjs +4 -0
- package/bin/lib/remediation.mjs +2 -2
- package/bin/lib/resolved-candidate-facts.mjs +144 -36
- package/bin/lib/scan-files.mjs +39 -0
- package/bin/lib/start-preview.mjs +3 -0
- package/bin/lib/upgrade-whats-new.mjs +3 -3
- package/bin/lib/violations.mjs +30 -0
- package/dist/{diagnosticCatalog-DA565Lja.d.ts → diagnosticCatalog-DVx_2RmF.d.ts} +1 -1
- package/dist/eslint/index.cjs +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/index.cjs +23 -23
- package/dist/index.d.ts +60 -14
- package/dist/index.js +17 -17
- package/dist/nestjs/index.cjs +1 -1
- package/dist/nestjs/index.js +1 -1
- package/dist/runtime/index.cjs +11 -11
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +11 -11
- package/docs/README.md +8 -6
- package/docs/agent-guide.md +29 -13
- package/docs/ai-gates.md +3 -1
- package/docs/arkorder.md +11 -4
- package/docs/configuration.md +7 -6
- package/docs/develop.md +3 -1
- package/docs/diagnostics.md +7 -7
- package/docs/enthusiast/README.md +6 -1
- package/docs/enthusiast/how-to-gallery-starter.md +2 -1
- package/docs/package-surface.md +6 -4
- package/docs/product-voice.md +32 -6
- package/docs/threat-model.md +2 -2
- package/docs/typescript-support.md +3 -3
- package/docs/use.md +18 -11
- package/package.json +1 -1
- package/server.json +2 -2
- package/templates/agent-skills/README.md +7 -4
- package/templates/agent-skills/ark-adopt/SKILL.md +9 -5
- package/templates/agent-skills/ark-architect/SKILL.md +5 -18
- package/templates/agent-skills/ark-autopilot/SKILL.md +8 -4
- package/templates/agent-skills/ark-contract/SKILL.md +9 -20
- package/templates/agent-skills/ark-coverage/SKILL.md +12 -8
- package/templates/agent-skills/ark-explain/SKILL.md +7 -3
- package/templates/agent-skills/ark-explore/SKILL.md +25 -4
- package/templates/agent-skills/ark-fix/SKILL.md +15 -20
- package/templates/agent-skills/ark-loop/SKILL.md +14 -20
- package/templates/agent-skills/ark-order/SKILL.md +200 -0
- package/templates/agent-skills/ark-place/SKILL.md +11 -8
- package/templates/agent-skills/ark-runtime/SKILL.md +17 -4
- package/templates/agent-skills/ark-think/SKILL.md +24 -126
- package/templates/agent-skills/ark-upgrade/SKILL.md +13 -2
- package/templates/skills/ark-adopt.md +9 -5
- package/templates/skills/ark-architect.md +5 -18
- package/templates/skills/ark-autopilot.md +8 -4
- package/templates/skills/ark-contract.md +9 -20
- package/templates/skills/ark-coverage.md +12 -8
- package/templates/skills/ark-explain.md +7 -3
- package/templates/skills/ark-explore.md +25 -4
- package/templates/skills/ark-fix.md +15 -20
- package/templates/skills/ark-loop.md +14 -20
- package/templates/skills/ark-order.md +200 -0
- package/templates/skills/ark-place.md +11 -8
- package/templates/skills/ark-runtime.md +17 -4
- package/templates/skills/ark-think.md +24 -126
- package/templates/skills/ark-upgrade.md +13 -2
- package/templates/tests/ark-adoption-gaps.test.ts +5 -4
|
@@ -12,6 +12,8 @@ import { composeMergePlanesHonesty, extraMergeTeethAllowed, isArkOrderRuleId, }
|
|
|
12
12
|
export const ARK_ORDER_DOCTOR_SCHEMA_VERSION = '1.0';
|
|
13
13
|
const RESIDUAL_RULE_CAP = 12;
|
|
14
14
|
export const ARKORDER_ONE_BREATH = 'Layers stop a bad import. ArkOrder stops rewriting a big product choice — like the billing plan — as if it were a seat count. Change those choices through a valve, not a generic update.';
|
|
15
|
+
/** First-contact next step. Reuses the billing gallery + existing doors. */
|
|
16
|
+
export const ARKORDER_FIRST_CONTACT_NEXT = 'Next: examples/arkorder-billing, then /ark-adopt to turn the extra on and /ark-order to wire one candidate.';
|
|
15
17
|
function closedMode(value) {
|
|
16
18
|
return value === 'enforced' || value === 'advisory' ? value : null;
|
|
17
19
|
}
|
|
@@ -129,7 +131,11 @@ export function formatArkOrderDoctorLines(section) {
|
|
|
129
131
|
if (!section || section.notAScore !== true)
|
|
130
132
|
return [];
|
|
131
133
|
if (section.active !== true) {
|
|
132
|
-
return [
|
|
134
|
+
return [
|
|
135
|
+
ARKORDER_ONE_BREATH,
|
|
136
|
+
'Off — Layers stay the same (not a score).',
|
|
137
|
+
ARKORDER_FIRST_CONTACT_NEXT,
|
|
138
|
+
];
|
|
133
139
|
}
|
|
134
140
|
const mode = section.mode ?? 'unknown';
|
|
135
141
|
const teeth = section.extraMergeTeeth === true ? 'armed' : 'not armed';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTML for the doctor ArkOrder advisory (report parity: data-advisory="arkOrder").
|
|
3
3
|
*/
|
|
4
|
-
import { ARKORDER_ONE_BREATH } from './ark-order-doctor.mjs';
|
|
4
|
+
import { ARKORDER_FIRST_CONTACT_NEXT, ARKORDER_ONE_BREATH } from './ark-order-doctor.mjs';
|
|
5
5
|
|
|
6
6
|
export function formatArkOrderHtml(section, esc) {
|
|
7
7
|
if (!section || typeof section !== 'object' || section.notAScore !== true) return '';
|
|
@@ -14,6 +14,7 @@ export function formatArkOrderHtml(section, esc) {
|
|
|
14
14
|
<p class="dim" style="margin:.15rem 0 .55rem;font-size:.88rem">
|
|
15
15
|
${ARKORDER_ONE_BREATH}
|
|
16
16
|
Off until you turn it on — Layers stay the same.
|
|
17
|
+
${ARKORDER_FIRST_CONTACT_NEXT}
|
|
17
18
|
</p>
|
|
18
19
|
${note}
|
|
19
20
|
</section>`;
|
|
@@ -338,6 +338,8 @@ files; never weaken the gate via subagents.
|
|
|
338
338
|
|
|
339
339
|
**Escapes, not a second curriculum.** Do **not** run overlapping skills for the same job.
|
|
340
340
|
Pick **one** primary skill. Prefer doctor top action #1 when unsure.
|
|
341
|
+
Filter: **Contener · Guiar · Ordenar** — contain the write, guide the next step, order leftover mess. Skills never enforce. First-class extras:
|
|
342
|
+
\`/ark-runtime\` (ArkRun) · \`/ark-order\` (ArkOrder).
|
|
341
343
|
|
|
342
344
|
| When | Invoke | Not this |
|
|
343
345
|
|------|--------|----------|
|
|
@@ -352,10 +354,11 @@ Pick **one** primary skill. Prefer doctor top action #1 when unsure.
|
|
|
352
354
|
| Gate violation on a change (small cluster) | \`/ark-autopilot\` | leftover \`/ark-fix\` |
|
|
353
355
|
| Drive plan **A** to goal.met | \`/ark-autopilot\` | leftover \`/ark-loop\` |
|
|
354
356
|
| Ark **fitness** only (governed%, gates, baseline, install gaps) | \`/ark-coverage\` | leftover design work (use single path above) |
|
|
355
|
-
| One design decision, 2–3 options | \`/ark-
|
|
357
|
+
| One design decision, 2–3 options | \`/ark-explore\` (one decision) | leftover \`/ark-think\`; apply → autopilot |
|
|
356
358
|
| Explain / HTML report tour | \`/ark-explain\` | explore |
|
|
357
359
|
| Bump arkgate + refresh hosts | \`/ark-upgrade\` | — |
|
|
358
|
-
| Optional runtime kernel evaluate | \`/ark-runtime\` |
|
|
360
|
+
| Optional runtime kernel evaluate | \`/ark-runtime\` | leftover \`/ark-run\` (not a skill); order plane → \`/ark-order\` |
|
|
361
|
+
| Optional ArkOrder evaluate / wire | \`/ark-order\` | extra off → \`/ark-adopt\`; skip grind → \`/ark-autopilot\` |
|
|
359
362
|
|
|
360
363
|
**Post-green door:** when doctor reports ENFORCE · leftover design work, the **primary** next action is the single Shape path above — not a choice among explore / coverage / think. Doctor JSON: \`postGreenPath\` / \`primaryNextAction\`.
|
|
361
364
|
|
|
@@ -405,6 +408,8 @@ ${projectionBlock}
|
|
|
405
408
|
|
|
406
409
|
**Primary path (do this):**
|
|
407
410
|
|
|
411
|
+
Contener · Guiar · Ordenar — contain the write, guide the next step, order leftover mess.
|
|
412
|
+
|
|
408
413
|
1. Status anytime: \`${doctorCmd}\` — what's wrong and what to do first.
|
|
409
414
|
2. Read the **Improvement compass** section (not a score). Name residual lenses in plain language when present (SoC, DIP, domain, …). Out-of-scope lenses (performance, app security tooling, full resilience) stay honest — do not invent Ark enforcement for them.
|
|
410
415
|
3. CLI-first: if the local CLI already resolved this project root, do not wait on MCP “still connecting”. Identity handshake is optional in that case. When you do trust MCP evidence: call \`ark_identity\` with \`project.expectedRoot\` set to this project's exact absolute root, then reuse that root plus the returned \`projectIdentity.projectId\` on every Ark MCP call. A descendant path is authoritative only with that matching id. Missing tool, non-\`matched\` binding, or wrong root means this is not proven to be the right project: restart the host and use the local CLI meanwhile.
|
|
@@ -493,6 +493,25 @@ export function summarizeDesignFitness(smells, ctx = {}) {
|
|
|
493
493
|
/** How many smell ids the green-run pointer names before it counts the rest. */
|
|
494
494
|
export const GREEN_PLAN_POINTER_MAX_IDS = 4;
|
|
495
495
|
|
|
496
|
+
/** When a smell has no evidence path, say so — do not leave the reader hunting. */
|
|
497
|
+
export const SMELL_UNATTRIBUTED =
|
|
498
|
+
'no file — leftover design, not a specific line';
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* One named smell with its first evidence path (or an explicit "no file").
|
|
502
|
+
* The pointer used to print only the id, so "1 design smell (domain-logic-in-ui)"
|
|
503
|
+
* could not answer "did my change cause this?"
|
|
504
|
+
*/
|
|
505
|
+
export function formatSmellPointerName(id, smells) {
|
|
506
|
+
const match = (smells || []).find((smell) => smell?.id === id);
|
|
507
|
+
const evidence = Array.isArray(match?.evidence)
|
|
508
|
+
? match.evidence.filter((entry) => typeof entry === 'string' && entry.trim())
|
|
509
|
+
: [];
|
|
510
|
+
if (evidence.length === 0) return `${id} (${SMELL_UNATTRIBUTED})`;
|
|
511
|
+
const extra = evidence.length - 1;
|
|
512
|
+
return extra > 0 ? `${id} at ${evidence[0]} (+${extra} more)` : `${id} at ${evidence[0]}`;
|
|
513
|
+
}
|
|
514
|
+
|
|
496
515
|
/**
|
|
497
516
|
* Name `--plan` on a run that passed.
|
|
498
517
|
*
|
|
@@ -528,7 +547,8 @@ export function formatGreenPlanPointer(smells, ctx = {}, planCommand = 'ark-chec
|
|
|
528
547
|
if (ids.length === 0) return null;
|
|
529
548
|
const shown = ids.slice(0, GREEN_PLAN_POINTER_MAX_IDS);
|
|
530
549
|
const hidden = ids.length - shown.length;
|
|
531
|
-
const
|
|
550
|
+
const namedIds = shown.map((id) => formatSmellPointerName(id, smells));
|
|
551
|
+
const named = hidden > 0 ? `${namedIds.join(', ')}, +${hidden} more` : namedIds.join(', ');
|
|
532
552
|
const plural = ids.length === 1 ? '' : 's';
|
|
533
553
|
const suppressed = ctx.suppressedCount ?? 0;
|
|
534
554
|
const opening =
|
|
@@ -32,7 +32,7 @@ function entry(ruleId, category, title, why, fix, extras) {
|
|
|
32
32
|
*/
|
|
33
33
|
export const DIAGNOSTIC_CATALOG = Object.freeze([
|
|
34
34
|
// ── layer / graph ────────────────────────────────────────────────────────
|
|
35
|
-
entry('LAYER_IMPORT_VIOLATION', 'layer', '
|
|
35
|
+
entry('LAYER_IMPORT_VIOLATION', 'layer', 'This import is not allowed', 'This file imported a folder it may not reach. The write doesn’t land. The same check fails the pull request.', 'Branch by import kind: constants/types/pure → adopt into DomainModel or SharedKernel (do not invent a port); kernel/events/bootstrap from Persistence → inject a port or move the map to SharedTypes (Persistence must not emit); define a port only when the target is a real use-case. Type-only edges use `import type`. Then preflight again. Do not weaken the layer rule without a hash-bound policy acknowledgement.'),
|
|
36
36
|
entry('LAYER_INTENT_REFERENCE_VIOLATION', 'layer', 'Intent referenced across a blocked layer edge', 'A string intent (or intent-like reference) names a layer that the file’s layer may not reach under the contract rules — the same plane as import edges, for event/intent coupling.', 'Reference that intent from a layer allowed to know about it (usually an adapter or application layer), or relocate the reference — then preflight again.'),
|
|
37
37
|
entry('LAYER_REFERENCE_VIOLATION', 'layer', 'Layer reference blocked (snippet / AI gate)', 'Snippet analysis found an intent or string reference that would couple layers in a direction the architecture profile forbids.', 'Move the reference to an allowed layer or introduce a port/event boundary, then re-run the snippet gate.'),
|
|
38
38
|
entry('CIRCULAR_DEPENDENCY', 'layer', 'Dependency cycle', 'Two or more modules import each other in a loop. Cycles make ownership unclear and break stable layer direction.', 'Extract the shared dependency into a third module, invert one edge behind a port, or merge units that are truly one — then preflight again.'),
|
|
@@ -75,7 +75,7 @@ export const DIAGNOSTIC_CATALOG = Object.freeze([
|
|
|
75
75
|
entry('ARKORDER_INFORMATION_BUDGET', 'arkorder', 'Projection observes a forbidden kind', 'h(ξ) allowedKinds includes a kind listed in informationBudget.cannotObserve. A scale may not look at what it was told not to see.', 'Cut that kind from the projector or from cannotObserve, then preflight again. Never mechanical-safe.'),
|
|
76
76
|
entry('ARKORDER_XI_TTL', 'arkorder', 'Slow key carries a freshness field', 'ξ named ttl/freshUntil/maxAge. Freshness belongs on σ. A slow parameter that expires per transaction is not slow.', 'Move freshness onto σ (freshUntil) and keep ξ stable, then preflight again. Never mechanical-safe.'),
|
|
77
77
|
entry('ARKORDER_STALE_SIGMA', 'arkorder', 'σ is stale', 'ingest ran after σ.freshUntil (or sigmaMaxAgeMs). ξ does not TTL.', 'Call refreshSigma and ingest again, or proposeRelease then apply(ProposeResult) if the pattern changed. Never mechanical-safe.'),
|
|
78
|
-
entry('ARKORDER_UNVALVED_RELEASE', 'arkorder', '
|
|
78
|
+
entry('ARKORDER_UNVALVED_RELEASE', 'arkorder', 'Second freeze without the valve', 'release() already froze the big choice. A later release() with a different value does not land. First freeze is release(); later change is proposeRelease then apply.', 'Change the choice with proposeRelease then apply. release() is only the first freeze. Never mechanical-safe.'),
|
|
79
79
|
// ── atomic preflight / change set ────────────────────────────────────────
|
|
80
80
|
entry('INVALID_CHANGE_PATH', 'preflight', 'Unsafe change path', 'A change set entry is not a safe, non-empty project-relative path (absolute, escape, empty, or NUL).', 'Use canonical project-relative paths only in the atomic change set, then preflight again.'),
|
|
81
81
|
entry('DUPLICATE_CHANGE_PATH', 'preflight', 'Duplicate path in change set', 'The atomic change set lists more than one operation for the same path.', 'Collapse to one create/update/delete per path, then preflight again.'),
|
|
@@ -22,6 +22,7 @@ import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
|
|
|
22
22
|
import { collectStewardNudge } from './team-parliament-io.mjs';
|
|
23
23
|
import { formatArkRunDoctorLines, summarizeArkRunSection } from './ark-run-doctor.mjs';
|
|
24
24
|
import {
|
|
25
|
+
ARKORDER_FIRST_CONTACT_NEXT,
|
|
25
26
|
ARKORDER_ONE_BREATH,
|
|
26
27
|
formatArkOrderDoctorLines,
|
|
27
28
|
summarizeArkOrderSection,
|
|
@@ -87,14 +88,21 @@ export function printCompactExtraDoctorLines(advisories, io) {
|
|
|
87
88
|
io.line(residual > 0 ? io.warn : ' ', `ArkRun: ${arkRun.mode || 'on'} · residual=${residual} · not a score`);
|
|
88
89
|
}
|
|
89
90
|
const arkOrder = advisories?.arkOrder;
|
|
90
|
-
if (arkOrder
|
|
91
|
+
if (arkOrder && arkOrder.notAScore === true) {
|
|
91
92
|
console.log('');
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
if (arkOrder.active === true) {
|
|
94
|
+
const residual = Number(arkOrder.residual?.count) || 0;
|
|
95
|
+
const keys =
|
|
96
|
+
Array.isArray(arkOrder.xiKeys) && arkOrder.xiKeys.length > 0
|
|
97
|
+
? arkOrder.xiKeys.join(', ')
|
|
98
|
+
: 'unnamed';
|
|
99
|
+
const mark = residual > 0 ? io.warn : ' ';
|
|
100
|
+
io.line(mark, ARKORDER_ONE_BREATH);
|
|
101
|
+
io.line(mark, `ArkOrder: ${arkOrder.mode || 'on'} · xiKeys=${keys} · residual=${residual} · not a score`);
|
|
102
|
+
} else {
|
|
103
|
+
io.line(' ', ARKORDER_ONE_BREATH);
|
|
104
|
+
io.line(' ', ARKORDER_FIRST_CONTACT_NEXT);
|
|
105
|
+
}
|
|
98
106
|
}
|
|
99
107
|
}
|
|
100
108
|
|
package/bin/lib/doctor-human.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { arkCommand } from '../ark-shared.mjs';
|
|
7
|
-
import { operatingModeTitle } from './product-copy.mjs';
|
|
7
|
+
import { NORTH_STAR_ONE_LINE, operatingModeTitle } from './product-copy.mjs';
|
|
8
8
|
import { isDoctorHealthyNothingToDo } from './post-green-path.mjs';
|
|
9
9
|
import { printParseHealthSection } from './parse-health.mjs';
|
|
10
10
|
import { printDoctorAdvisories, printCompactExtraDoctorLines } from './doctor-advisories.mjs';
|
|
@@ -73,6 +73,7 @@ export function printDoctorCompactHuman(view) {
|
|
|
73
73
|
const skippableCi = ciNotFailClosedNotice(view);
|
|
74
74
|
|
|
75
75
|
console.log(color.bold(`Ark doctor — ${path.basename(path.resolve(root)) || '.'}`));
|
|
76
|
+
console.log(color.dim(NORTH_STAR_ONE_LINE));
|
|
76
77
|
if (!analysisComplete) line(warn, analysisIncompleteStatement(completeness));
|
|
77
78
|
printParseHealthSection(doctorAdvisories.parseHealth, { color, warn, line });
|
|
78
79
|
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
* First-run CLI help (setup + check). Encyclopedia text stays behind --help --all.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { NORTH_STAR_ONE_LINE } from './product-copy.mjs';
|
|
6
|
+
|
|
5
7
|
export function setupUsage() {
|
|
6
8
|
return `arkgate (alias ark) — Write. Check. Ship.
|
|
7
9
|
When the agent writes a bad import, the write doesn’t land. Same check fails the PR.
|
|
10
|
+
${NORTH_STAR_ONE_LINE}
|
|
8
11
|
|
|
9
12
|
arkgate start preview (no writes)
|
|
10
13
|
arkgate start --apply write host + CI setup
|
|
@@ -30,6 +33,7 @@ Every flag: arkgate --help --all
|
|
|
30
33
|
export function setupUsageAll() {
|
|
31
34
|
return `arkgate (alias ark) — Write. Check. Ship.
|
|
32
35
|
When the agent writes a bad import, the write doesn’t land. Same check fails the PR.
|
|
36
|
+
${NORTH_STAR_ONE_LINE}
|
|
33
37
|
|
|
34
38
|
Usage:
|
|
35
39
|
arkgate start [--root <project>] [--tools <host>] [--require-write-hook <host>] [--install] [--apply] [--json]
|
|
@@ -158,9 +162,12 @@ export async function withSensorsPartialModeHonesty(args, run) {
|
|
|
158
162
|
|
|
159
163
|
export function checkUsage() {
|
|
160
164
|
return [
|
|
161
|
-
'arkgate-check (alias ark-check) —
|
|
165
|
+
'arkgate-check (alias ark-check) — Write. Check. Ship.',
|
|
166
|
+
'When the agent writes a bad import, the write doesn’t land. Same check fails the PR.',
|
|
167
|
+
NORTH_STAR_ONE_LINE,
|
|
162
168
|
'',
|
|
163
169
|
' arkgate-check --doctor where you are: one status light, one next action',
|
|
170
|
+
' arkgate-check --changed --base <ref> local / pre-push: checks touched files only',
|
|
164
171
|
' arkgate-check --strict-merge CI / merge gate (required GitHub status)',
|
|
165
172
|
' arkgate-check --sensors which sensors can ever be enforced (does not run analysis)',
|
|
166
173
|
'',
|
|
@@ -170,7 +177,9 @@ export function checkUsage() {
|
|
|
170
177
|
|
|
171
178
|
export function checkUsageAll() {
|
|
172
179
|
return [
|
|
173
|
-
'arkgate-check (alias ark-check) —
|
|
180
|
+
'arkgate-check (alias ark-check) — Write. Check. Ship.',
|
|
181
|
+
'When the agent writes a bad import, the write doesn’t land. Same check fails the PR.',
|
|
182
|
+
NORTH_STAR_ONE_LINE,
|
|
174
183
|
'',
|
|
175
184
|
'Usage: arkgate-check | ark-check (identical bins; product name ArkGate)',
|
|
176
185
|
' arkgate-check --version',
|
package/bin/lib/gate-files.mjs
CHANGED
|
@@ -257,6 +257,42 @@ export function hasArkMcpRegistration(root, relativePath = '.mcp.json') {
|
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Shared MCP JSON destinations written by install-agent-gates (catalog + host copies).
|
|
262
|
+
* Upsert `mcpServers.ark` without clobbering sibling servers — same contract as
|
|
263
|
+
* mergeOpencodeArkMcp / mergeCursorArkHook.
|
|
264
|
+
*/
|
|
265
|
+
export const MCP_JSON_GATE_FILES = ['.mcp.json', '.cursor/mcp.json', '.agents/mcp_config.json'];
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Upsert mcpServers.ark into an existing MCP JSON document.
|
|
269
|
+
* Preserves sibling servers and unknown top-level keys. Returns null if unreadable.
|
|
270
|
+
*/
|
|
271
|
+
export function mergeArkMcpJson(existingText, generatedText) {
|
|
272
|
+
let existing;
|
|
273
|
+
let generated;
|
|
274
|
+
try {
|
|
275
|
+
existing = existingText && existingText.trim() ? JSON.parse(existingText) : {};
|
|
276
|
+
generated = JSON.parse(generatedText);
|
|
277
|
+
} catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
if (!existing || typeof existing !== 'object' || Array.isArray(existing)) return null;
|
|
281
|
+
if (!generated || typeof generated !== 'object' || Array.isArray(generated)) return null;
|
|
282
|
+
const generatedArk = generated.mcpServers?.ark;
|
|
283
|
+
if (!generatedArk || typeof generatedArk !== 'object' || Array.isArray(generatedArk)) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const servers =
|
|
287
|
+
existing.mcpServers &&
|
|
288
|
+
typeof existing.mcpServers === 'object' &&
|
|
289
|
+
!Array.isArray(existing.mcpServers)
|
|
290
|
+
? { ...existing.mcpServers }
|
|
291
|
+
: {};
|
|
292
|
+
const next = { ...existing, mcpServers: { ...servers, ark: generatedArk } };
|
|
293
|
+
return `${JSON.stringify(next, null, 2)}\n`;
|
|
294
|
+
}
|
|
295
|
+
|
|
260
296
|
function commandArkMcpArgs(command) {
|
|
261
297
|
if (typeof command !== 'string') return null;
|
|
262
298
|
const words = [];
|
|
@@ -747,12 +783,13 @@ export function writeTemplate(root, relativePath, content, force) {
|
|
|
747
783
|
return { relativePath, status: 'skipped-self-hosted' };
|
|
748
784
|
}
|
|
749
785
|
if (existing && !isArkAgentsContent(existing)) {
|
|
750
|
-
// Never
|
|
751
|
-
//
|
|
786
|
+
// Never replace a project-owned AGENTS.md — even with --force.
|
|
787
|
+
// Merge the Ark section once when missing so start / install-agent-gates
|
|
788
|
+
// leave a contract doctor accepts (issue #210). Subsequent runs leave it.
|
|
752
789
|
const hasArkSection =
|
|
753
790
|
/#\s*Ark(Gate)?\s+Enforcement\b/.test(existing) ||
|
|
754
791
|
/ark\.config\.json is authoritative/i.test(existing);
|
|
755
|
-
if (
|
|
792
|
+
if (isArkAgentsContent(content) && !hasArkSection) {
|
|
756
793
|
try {
|
|
757
794
|
const merged = `${existing.replace(/\s*$/, '')}\n\n---\n\n${content}`;
|
|
758
795
|
ensureDirForFile(fullPath);
|
|
@@ -37,6 +37,8 @@ import {
|
|
|
37
37
|
hasCheckArchitectureScript,
|
|
38
38
|
ensureTypecheckScript,
|
|
39
39
|
compactRouterHost,
|
|
40
|
+
mergeArkMcpJson,
|
|
41
|
+
MCP_JSON_GATE_FILES,
|
|
40
42
|
writeTemplate,
|
|
41
43
|
} from './gate-files.mjs';
|
|
42
44
|
import {
|
|
@@ -573,6 +575,27 @@ export function runInstallAgentGates(args) {
|
|
|
573
575
|
// Upsert ark-write-gate without requiring --force; never wipe sibling named hooks.
|
|
574
576
|
return writeTemplate(root, relativePath, merged, true);
|
|
575
577
|
}
|
|
578
|
+
if (MCP_JSON_GATE_FILES.includes(relativePath)) {
|
|
579
|
+
const fullPath = path.join(root, relativePath);
|
|
580
|
+
let existing = '';
|
|
581
|
+
try {
|
|
582
|
+
existing = fs.readFileSync(fullPath, 'utf8');
|
|
583
|
+
} catch {
|
|
584
|
+
// Missing MCP JSON → write generated ark server.
|
|
585
|
+
}
|
|
586
|
+
if (!existing) {
|
|
587
|
+
return writeTemplate(root, relativePath, content, true);
|
|
588
|
+
}
|
|
589
|
+
const merged = mergeArkMcpJson(existing, content);
|
|
590
|
+
if (merged == null) {
|
|
591
|
+
return args.force
|
|
592
|
+
? writeTemplate(root, relativePath, content, true)
|
|
593
|
+
: { relativePath, status: 'skipped-non-ark' };
|
|
594
|
+
}
|
|
595
|
+
if (merged === existing) return { relativePath, status: 'skipped' };
|
|
596
|
+
// Upsert mcpServers.ark without requiring --force; never wipe sibling servers.
|
|
597
|
+
return writeTemplate(root, relativePath, merged, true);
|
|
598
|
+
}
|
|
576
599
|
return writeTemplate(
|
|
577
600
|
root,
|
|
578
601
|
relativePath,
|
|
@@ -314,7 +314,7 @@ export function emitHostAllow(output, { antigravityStyle, cursorStyle }) {
|
|
|
314
314
|
*/
|
|
315
315
|
export function formatWriteGateDeny({ file, reason, ruleId, nextAction, extraLines = [] }) {
|
|
316
316
|
const target = file || 'this write';
|
|
317
|
-
const why = String(reason || '
|
|
317
|
+
const why = String(reason || 'a bad import — the write doesn’t land').replace(/\s+/g, ' ').trim();
|
|
318
318
|
const next =
|
|
319
319
|
nextAction && /place|move|import|port/i.test(nextAction)
|
|
320
320
|
? nextAction
|
package/bin/lib/product-copy.mjs
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* common software words. See docs/product-voice.md.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
/** First-contact north star. Ordinary English. Spanish names once. */
|
|
10
|
+
export const NORTH_STAR_ONE_LINE =
|
|
11
|
+
'Contener · Guiar · Ordenar — contain the write, guide the next step, order leftover mess.';
|
|
12
|
+
|
|
9
13
|
/** Status-light leftover-design qualifier (was “design-weak”). */
|
|
10
14
|
export const LEFTOVER_DESIGN_LABEL = 'leftover design work';
|
|
11
15
|
|
package/bin/lib/remediation.mjs
CHANGED
|
@@ -249,7 +249,7 @@ export function deterministicNextAction(violation) {
|
|
|
249
249
|
? `Don't write ${violation.target} from a use-case. Take the event in, or change that choice through the valve (proposeRelease then apply), not a generic update.`
|
|
250
250
|
: "Don't write a named product choice from a use-case. Take the event in, or change that choice through the valve (proposeRelease then apply), not a generic update.";
|
|
251
251
|
case 'ARKORDER_UNVALVED_RELEASE':
|
|
252
|
-
return 'Change
|
|
252
|
+
return 'Change the choice with proposeRelease then apply. release() is only the first freeze. Never mechanical-safe.';
|
|
253
253
|
default:
|
|
254
254
|
if (typeof violation.ruleId === 'string' && violation.ruleId.startsWith('ARKRULE_')) {
|
|
255
255
|
return `Fix the ArkRule ${typeof violation.arkruleId === 'string' ? violation.arkruleId : violation.ruleId}, then preflight again.`;
|
|
@@ -544,7 +544,7 @@ export function enrichViolationWithFixClass(violation) {
|
|
|
544
544
|
: violation.ruleId === 'ARKORDER_STALE_SIGMA'
|
|
545
545
|
? 'Refresh σ. ξ does not expire.'
|
|
546
546
|
: violation.ruleId === 'ARKORDER_UNVALVED_RELEASE'
|
|
547
|
-
? 'The
|
|
547
|
+
? 'The choice is already frozen. proposeRelease then apply — do not call release() again with a different value.'
|
|
548
548
|
: 'Call createOrderPlane from arkgate/order in a listed plane root so the app actually freezes a pattern.';
|
|
549
549
|
break;
|
|
550
550
|
default:
|
|
@@ -211,32 +211,67 @@ function canonicalOverlayPath(
|
|
|
211
211
|
return [...candidates].sort()[0] ?? requested;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
function
|
|
214
|
+
function discoverScopedFiles(root, config, scopeRelatives, observeInput) {
|
|
215
|
+
const discovered = [];
|
|
216
|
+
const seen = new Set();
|
|
217
|
+
for (const raw of scopeRelatives ?? []) {
|
|
218
|
+
let relative;
|
|
219
|
+
try {
|
|
220
|
+
relative = canonicalProjectPath(
|
|
221
|
+
String(raw || '')
|
|
222
|
+
.replace(/\\/g, '/')
|
|
223
|
+
.replace(/^\.\//, '')
|
|
224
|
+
);
|
|
225
|
+
} catch {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (seen.has(relative) || !isGovernableSourceFile(path.basename(relative))) continue;
|
|
229
|
+
if (!isIncluded(relative, config.include) || isScanExcludedRelative(relative, config)) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const absolute = path.join(root, ...relative.split('/'));
|
|
233
|
+
if (!fs.existsSync(absolute)) continue;
|
|
234
|
+
seen.add(relative);
|
|
235
|
+
observeResolvedInput(observeInput, absolute, 'realpath');
|
|
236
|
+
discovered.push({
|
|
237
|
+
absolute,
|
|
238
|
+
real: fs.realpathSync(absolute),
|
|
239
|
+
relative,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return discovered.sort((left, right) =>
|
|
243
|
+
left.relative < right.relative ? -1 : left.relative > right.relative ? 1 : 0
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function collectCandidateFiles(root, config, changes, observeInput, scopeRelatives) {
|
|
215
248
|
const files = new Map();
|
|
216
249
|
const directoryAliases = new Map();
|
|
217
250
|
const expandedAliasDirectories = new Set();
|
|
218
|
-
const discovered = (
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
251
|
+
const discovered = Array.isArray(scopeRelatives)
|
|
252
|
+
? discoverScopedFiles(root, config, scopeRelatives, observeInput)
|
|
253
|
+
: (config.include ?? [])
|
|
254
|
+
.flatMap((entry) =>
|
|
255
|
+
collectGovernedFiles(root, { ...config, include: [entry] }, {
|
|
256
|
+
observeInput,
|
|
257
|
+
onDirectory(absolute, real) {
|
|
258
|
+
let relative = normalize(path.relative(root, absolute));
|
|
259
|
+
if (relative === '.') relative = '';
|
|
260
|
+
rememberDirectoryAlias(directoryAliases, real, relative, absolute);
|
|
261
|
+
},
|
|
262
|
+
})
|
|
263
|
+
)
|
|
264
|
+
.map((absolute) => {
|
|
265
|
+
observeResolvedInput(observeInput, absolute, 'realpath');
|
|
266
|
+
return {
|
|
267
|
+
absolute,
|
|
268
|
+
real: fs.realpathSync(absolute),
|
|
269
|
+
relative: canonicalProjectPath(normalize(path.relative(root, absolute))),
|
|
270
|
+
};
|
|
271
|
+
})
|
|
272
|
+
.sort((left, right) =>
|
|
273
|
+
left.relative < right.relative ? -1 : left.relative > right.relative ? 1 : 0
|
|
274
|
+
);
|
|
240
275
|
const canonicalByRealpath = new Map();
|
|
241
276
|
for (const candidate of discovered) {
|
|
242
277
|
if (!canonicalByRealpath.has(candidate.real)) canonicalByRealpath.set(candidate.real, candidate);
|
|
@@ -582,7 +617,8 @@ function compilerContext(ts, root, tsconfig, candidateFiles, observeInput) {
|
|
|
582
617
|
const optionsByPath = new Map();
|
|
583
618
|
const configInputsByPath = new Map();
|
|
584
619
|
const reasons = [];
|
|
585
|
-
|
|
620
|
+
const loadConfigOptions = (configPath) => {
|
|
621
|
+
if (optionsByPath.has(configPath)) return optionsByPath.get(configPath);
|
|
586
622
|
const configContents = new Map();
|
|
587
623
|
const readConfig = (fileName) => {
|
|
588
624
|
observeResolvedInput(observeInput, fileName, 'tsconfig');
|
|
@@ -605,15 +641,16 @@ function compilerContext(ts, root, tsconfig, candidateFiles, observeInput) {
|
|
|
605
641
|
});
|
|
606
642
|
optionsByPath.set(configPath, {});
|
|
607
643
|
configInputsByPath.set(configPath, configContents);
|
|
608
|
-
|
|
644
|
+
return {};
|
|
609
645
|
}
|
|
610
646
|
const parsed = ts.parseJsonConfigFileContent(
|
|
611
647
|
read.config,
|
|
612
648
|
{
|
|
613
649
|
useCaseSensitiveFileNames: ts.sys?.useCaseSensitiveFileNames ?? true,
|
|
614
|
-
readDirectory(
|
|
615
|
-
observeResolvedInput(observeInput,
|
|
616
|
-
|
|
650
|
+
readDirectory(dir) {
|
|
651
|
+
observeResolvedInput(observeInput, dir, 'tsconfig-directory');
|
|
652
|
+
// Options-only parse: `fileNames` are unused. Skip the include walk (#205).
|
|
653
|
+
return [];
|
|
617
654
|
},
|
|
618
655
|
fileExists(fileName) {
|
|
619
656
|
observeResolvedInput(observeInput, fileName, 'tsconfig-exists');
|
|
@@ -637,11 +674,19 @@ function compilerContext(ts, root, tsconfig, candidateFiles, observeInput) {
|
|
|
637
674
|
}
|
|
638
675
|
optionsByPath.set(configPath, parsed.options ?? {});
|
|
639
676
|
configInputsByPath.set(configPath, configContents);
|
|
640
|
-
|
|
677
|
+
return parsed.options ?? {};
|
|
678
|
+
};
|
|
679
|
+
for (const configPath of configPaths) loadConfigOptions(configPath);
|
|
641
680
|
|
|
642
681
|
const optionsFor = (fileName) => {
|
|
643
|
-
const
|
|
644
|
-
|
|
682
|
+
const resolved = path.resolve(fileName);
|
|
683
|
+
let configPath = configByFile.get(resolved);
|
|
684
|
+
if (!configPath) {
|
|
685
|
+
configPath = explicitPath
|
|
686
|
+
?? nearestTsconfig(root, resolved, nearestConfigByDirectory, observeInput);
|
|
687
|
+
if (configPath) configByFile.set(resolved, configPath);
|
|
688
|
+
}
|
|
689
|
+
return configPath ? loadConfigOptions(configPath) ?? {} : {};
|
|
645
690
|
};
|
|
646
691
|
const configs = configPaths.map((configPath) => ({
|
|
647
692
|
path: configLabel(root, configPath, externalAnchor),
|
|
@@ -1036,6 +1081,19 @@ function unavailableFacts(config, ts, reason) {
|
|
|
1036
1081
|
});
|
|
1037
1082
|
}
|
|
1038
1083
|
|
|
1084
|
+
function loadOneCandidateFile(root, config, relative, observeInput) {
|
|
1085
|
+
const discovered = discoverScopedFiles(root, config, [relative], observeInput);
|
|
1086
|
+
const found = discovered[0];
|
|
1087
|
+
if (!found) return null;
|
|
1088
|
+
observeResolvedInput(observeInput, found.absolute, 'source');
|
|
1089
|
+
return {
|
|
1090
|
+
path: found.relative,
|
|
1091
|
+
absolute: path.resolve(found.absolute),
|
|
1092
|
+
real: path.resolve(found.real),
|
|
1093
|
+
content: fs.readFileSync(found.absolute, 'utf8'),
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1039
1097
|
/** Resolve one complete candidate tree (base or virtual overlay) into versioned neutral facts. */
|
|
1040
1098
|
export function resolveCandidateFacts({
|
|
1041
1099
|
root,
|
|
@@ -1044,6 +1102,7 @@ export function resolveCandidateFacts({
|
|
|
1044
1102
|
tsconfig,
|
|
1045
1103
|
changes = [],
|
|
1046
1104
|
observeInput,
|
|
1105
|
+
scopeFiles,
|
|
1047
1106
|
}) {
|
|
1048
1107
|
if (!ts?.createSourceFile || !ts?.resolveModuleName) {
|
|
1049
1108
|
return unavailableFacts(config, ts, 'No API-compatible TypeScript resolver is available.');
|
|
@@ -1053,10 +1112,17 @@ export function resolveCandidateFacts({
|
|
|
1053
1112
|
let candidateFiles;
|
|
1054
1113
|
let canonicalChanges;
|
|
1055
1114
|
let compiler;
|
|
1115
|
+
const scoped = Array.isArray(scopeFiles);
|
|
1056
1116
|
try {
|
|
1057
1117
|
observeResolvedInput(observeInput, root, 'realpath');
|
|
1058
1118
|
canonicalRoot = fs.realpathSync(root);
|
|
1059
|
-
const candidate = collectCandidateFiles(
|
|
1119
|
+
const candidate = collectCandidateFiles(
|
|
1120
|
+
canonicalRoot,
|
|
1121
|
+
config,
|
|
1122
|
+
changes,
|
|
1123
|
+
observeInput,
|
|
1124
|
+
scoped ? scopeFiles : undefined
|
|
1125
|
+
);
|
|
1060
1126
|
candidateFiles = candidate.files;
|
|
1061
1127
|
canonicalChanges = candidate.changes;
|
|
1062
1128
|
compiler = compilerContext(ts, canonicalRoot, tsconfig, candidateFiles, observeInput);
|
|
@@ -1106,7 +1172,8 @@ export function resolveCandidateFacts({
|
|
|
1106
1172
|
const compositionRootPatterns = [...(config.arkRun?.compositionRoots ?? [])];
|
|
1107
1173
|
const planeRootPatterns = [...(config.arkOrder?.planeRoots ?? [])];
|
|
1108
1174
|
|
|
1109
|
-
|
|
1175
|
+
const seedPathSet = new Set(candidateFiles.map((file) => file.path));
|
|
1176
|
+
const ingest = (candidate, fullExtract) => {
|
|
1110
1177
|
const sourceFile = ts.createSourceFile(
|
|
1111
1178
|
candidate.absolute,
|
|
1112
1179
|
candidate.content,
|
|
@@ -1158,6 +1225,7 @@ export function resolveCandidateFacts({
|
|
|
1158
1225
|
message: `${candidate.path} has ${parseDiagnosticCount} TypeScript parse diagnostic(s).`,
|
|
1159
1226
|
});
|
|
1160
1227
|
}
|
|
1228
|
+
if (!fullExtract) return;
|
|
1161
1229
|
const forbiddenUses = mayContainForbiddenCapability(ts, sourceFile, forbiddenGlobals)
|
|
1162
1230
|
? collectForbiddenCapabilityUses(ts, sourceFile, forbiddenGlobals)
|
|
1163
1231
|
: [];
|
|
@@ -1244,10 +1312,50 @@ export function resolveCandidateFacts({
|
|
|
1244
1312
|
// Never fail the resolver for ArkOrder release key-count extraction.
|
|
1245
1313
|
}
|
|
1246
1314
|
}
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
for (const candidate of candidateFiles) ingest(candidate, true);
|
|
1318
|
+
|
|
1319
|
+
if (scoped) {
|
|
1320
|
+
const loaded = new Set(candidateFiles.map((file) => file.path));
|
|
1321
|
+
let pending = [...candidateFiles];
|
|
1322
|
+
while (pending.length > 0) {
|
|
1323
|
+
const discovered = [];
|
|
1324
|
+
for (const source of pending) {
|
|
1325
|
+
const parsedFile = parsed.get(source.path);
|
|
1326
|
+
if (!parsedFile) continue;
|
|
1327
|
+
for (const dependency of parsedFile.dependencies) {
|
|
1328
|
+
const resolved = resolveDependency(
|
|
1329
|
+
ts,
|
|
1330
|
+
dependency,
|
|
1331
|
+
parsedFile.candidate.absolute,
|
|
1332
|
+
compiler.optionsFor(parsedFile.candidate.absolute),
|
|
1333
|
+
host
|
|
1334
|
+
);
|
|
1335
|
+
if (resolved.resolution !== 'resolved-project' || !resolved.target) continue;
|
|
1336
|
+
if (loaded.has(resolved.target)) continue;
|
|
1337
|
+
const extra = loadOneCandidateFile(
|
|
1338
|
+
canonicalRoot,
|
|
1339
|
+
config,
|
|
1340
|
+
resolved.target,
|
|
1341
|
+
observeInput
|
|
1342
|
+
);
|
|
1343
|
+
if (!extra) continue;
|
|
1344
|
+
loaded.add(extra.path);
|
|
1345
|
+
candidateFiles.push(extra);
|
|
1346
|
+
ingest(extra, false);
|
|
1347
|
+
discovered.push(extra);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
pending = discovered;
|
|
1351
|
+
}
|
|
1247
1352
|
}
|
|
1248
1353
|
|
|
1354
|
+
const extractCandidates = scoped
|
|
1355
|
+
? candidateFiles.filter((file) => seedPathSet.has(file.path))
|
|
1356
|
+
: candidateFiles;
|
|
1249
1357
|
const admittedTypeNames = new Set(classShapes.map((shape) => shape.className));
|
|
1250
|
-
for (const candidate of
|
|
1358
|
+
for (const candidate of extractCandidates) {
|
|
1251
1359
|
if (!/\.(tsx?|mts|cts)$/i.test(candidate.path)) continue;
|
|
1252
1360
|
try {
|
|
1253
1361
|
arkRunManagedNews.push(
|
|
@@ -1264,7 +1372,7 @@ export function resolveCandidateFacts({
|
|
|
1264
1372
|
|
|
1265
1373
|
if (planeRootPatterns.length > 0) {
|
|
1266
1374
|
const factoryFiles = new Set(arkOrderPlaneCalls.map((call) => call.file));
|
|
1267
|
-
for (const candidate of
|
|
1375
|
+
for (const candidate of extractCandidates) {
|
|
1268
1376
|
for (const pattern of planeRootPatterns) {
|
|
1269
1377
|
try {
|
|
1270
1378
|
if (!globToRegExp(pattern).test(candidate.path)) continue;
|
|
@@ -1284,7 +1392,7 @@ export function resolveCandidateFacts({
|
|
|
1284
1392
|
const factoryFiles = new Set(
|
|
1285
1393
|
arkRunKernelCalls.filter((call) => call.kind === 'factory').map((call) => call.file)
|
|
1286
1394
|
);
|
|
1287
|
-
for (const candidate of
|
|
1395
|
+
for (const candidate of extractCandidates) {
|
|
1288
1396
|
for (const pattern of compositionRootPatterns) {
|
|
1289
1397
|
try {
|
|
1290
1398
|
if (!globToRegExp(pattern).test(candidate.path)) continue;
|