arkgate 4.8.23 → 4.8.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/README.md +7 -5
- package/bin/ark-check-runtime.mjs +24 -77
- package/bin/ark-dashboard.mjs +56 -53
- package/bin/ark-layer-match.mjs +596 -112
- package/bin/ark-mcp-runtime.mjs +412 -138
- package/bin/ark-mcp.mjs +68 -4
- package/bin/ark-shared.mjs +39 -0
- package/bin/ark.mjs +65 -97
- package/bin/lib/adapter-contract-types.mjs +5 -0
- package/bin/lib/adapter-contract.mjs +15 -0
- package/bin/lib/agent-projection-command.mjs +5 -1
- package/bin/lib/analysis-engine.mjs +15 -11
- package/bin/lib/architecture-scan.mjs +50 -6
- package/bin/lib/ark-order-doctor.mjs +1 -1
- package/bin/lib/ark-order-facts.mjs +410 -15
- package/bin/lib/ark-order-invariants.mjs +51 -9
- package/bin/lib/ark-order-sensors.mjs +28 -101
- package/bin/lib/ark-order-types.mjs +2 -0
- package/bin/lib/ark-run-doctor.mjs +14 -0
- package/bin/lib/ark-run-facts.mjs +434 -11
- package/bin/lib/ark-run-sensors.mjs +5 -3
- package/bin/lib/arkgate-pins.mjs +91 -0
- package/bin/lib/arkrules-sensors.mjs +50 -249
- package/bin/lib/check-args.mjs +23 -1
- package/bin/lib/ci-and-commands.mjs +16 -6
- package/bin/lib/class-source-scan.mjs +479 -0
- package/bin/lib/codex-home.mjs +4 -2
- package/bin/lib/config-contract-slices.mjs +701 -0
- package/bin/lib/config-contract.mjs +5 -493
- package/bin/lib/config-warnings.mjs +32 -0
- package/bin/lib/dashboard-cli.mjs +62 -0
- package/bin/lib/design-delta.mjs +1 -1
- package/bin/lib/diagnostic-catalog.mjs +16 -9
- package/bin/lib/doctor-advisories.mjs +25 -1
- package/bin/lib/doctor-next-actions.mjs +17 -2
- package/bin/lib/doctor-plan.mjs +6 -8
- package/bin/lib/effective-contract-load.mjs +121 -59
- package/bin/lib/enforcement-profiles.mjs +40 -4
- package/bin/lib/first-run-help.mjs +3 -2
- package/bin/lib/flat-parent-importers.mjs +137 -0
- package/bin/lib/flat-parent-pilot.mjs +40 -100
- package/bin/lib/gate-files.mjs +29 -2
- package/bin/lib/git-change-scope.mjs +109 -0
- package/bin/lib/github-enforcement.mjs +1 -1
- package/bin/lib/hook-templates.mjs +18 -16
- package/bin/lib/host-support-matrix.mjs +3 -1
- package/bin/lib/import-resolve.mjs +13 -1
- package/bin/lib/install-migrate.mjs +50 -73
- package/bin/lib/invariant-coverage.mjs +59 -11
- package/bin/lib/mcp-adoption.mjs +9 -1
- package/bin/lib/mcp-hook-payload.mjs +401 -61
- package/bin/lib/package-manager.mjs +59 -1
- package/bin/lib/passthrough-commands.mjs +83 -0
- package/bin/lib/persistence-write-hint.mjs +81 -0
- package/bin/lib/pin-dependent-gates.mjs +117 -0
- package/bin/lib/policy-delta-io.mjs +168 -10
- package/bin/lib/prepare-change.mjs +120 -6
- package/bin/lib/prepare-write.mjs +1 -0
- package/bin/lib/presets.mjs +3 -5
- package/bin/lib/remediation.mjs +23 -4
- package/bin/lib/resolved-candidate-facts.mjs +315 -55
- package/bin/lib/rules-inventory.mjs +22 -3
- package/bin/lib/rules-under-contract.mjs +179 -5
- package/bin/lib/scan-files.mjs +32 -0
- package/bin/lib/snippet-analysis.mjs +140 -6
- package/bin/lib/source-policy.mjs +34 -0
- package/bin/lib/stable-hash.mjs +90 -0
- package/bin/lib/start-install-recovery.mjs +71 -1
- package/bin/lib/start-preview.mjs +30 -0
- package/bin/lib/status-command.mjs +33 -4
- package/bin/lib/status-manifest.mjs +32 -4
- package/bin/lib/team-parliament-io.mjs +77 -75
- package/bin/lib/team-parliament.mjs +6 -1
- package/bin/lib/write-path-capabilities.mjs +4 -2
- package/dist/chunk-A5EIY3HI.cjs +5 -0
- package/dist/chunk-MBXCFLO4.js +16 -0
- package/dist/chunk-RNQHRDCQ.cjs +16 -0
- package/dist/chunk-SVSQVGFJ.js +5 -0
- package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-BJbaeoj4.d.cts} +18 -3
- package/dist/configTypes-BJbaeoj4.d.ts +246 -0
- package/dist/eslint/index.cjs +17 -4
- package/dist/eslint/index.d.cts +359 -0
- package/dist/eslint/index.d.ts +130 -156
- package/dist/eslint/index.js +10 -4
- package/dist/index.cjs +35 -34
- package/dist/index.d.cts +5475 -0
- package/dist/index.d.ts +3312 -299
- package/dist/index.js +35 -34
- 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} +804 -290
- package/dist/runtime/index.d.ts +2678 -108
- package/dist/runtime/index.js +13 -30
- package/dist/{types-Bdj32QwA.d.ts → types-DRinG-vT.d.cts} +576 -140
- package/dist/types-DRinG-vT.d.ts +1792 -0
- package/docs/README.md +2 -1
- package/docs/agent-guide.md +84 -15
- package/docs/ai-gates.md +160 -26
- package/docs/arkorder.md +32 -15
- package/docs/brownfield-adoption.md +5 -1
- package/docs/configuration.md +196 -26
- package/docs/develop.md +2 -1
- package/docs/diagnostics.md +96 -19
- package/docs/package-surface.md +31 -8
- package/docs/product-voice.md +1 -1
- package/package.json +59 -18
- package/schemas/ark.analysis-result.schema.json +15 -0
- package/schemas/ark.config.schema.json +47 -10
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
- package/schemas/ark.status-manifest.schema.json +22 -0
- package/server.json +3 -9
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +4 -1
- package/templates/agent-skills/ark-order/SKILL.md +4 -2
- package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
- package/templates/arkrules/PersistenceAdapters.json +1 -1
- package/templates/arkrules/PresentationAdapters.json +1 -1
- package/templates/skills/ark-adopt.md +4 -1
- package/templates/skills/ark-order.md +4 -2
- package/templates/skills/ark-runtime.md +6 -1
- package/dist/types-Ik_OakcI.d.ts +0 -183
|
@@ -2,15 +2,22 @@
|
|
|
2
2
|
* Architecture check pipeline: content scan → import graph → layer edges → cycles.
|
|
3
3
|
* Extracted from ark-check entry (R3). Entry remains orchestration + presentation.
|
|
4
4
|
*/
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import { summarizeParseHealth } from './parse-health.mjs';
|
|
7
8
|
import {
|
|
8
9
|
analyzeTrustedResolvedProject,
|
|
10
|
+
configVersionFloors,
|
|
9
11
|
loadContract,
|
|
10
12
|
} from './analysis-engine.mjs';
|
|
13
|
+
import { pathUnderSharedRoot, sharedImportsSliceMode } from '../ark-layer-match.mjs';
|
|
14
|
+
import { collectArkgatePins, runningArkgateVersion } from './arkgate-pins.mjs';
|
|
11
15
|
import { effectiveAnalysisConfig } from './analysis-policy.mjs';
|
|
12
16
|
import { resolveCandidateFacts } from './resolved-candidate-facts.mjs';
|
|
13
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
arkRulesDriftWarnings,
|
|
19
|
+
loadEffectiveArkRulesFromDisk,
|
|
20
|
+
} from './effective-contract-load.mjs';
|
|
14
21
|
import {
|
|
15
22
|
coverageOptionsFromConfig,
|
|
16
23
|
invariantIdsFromCatalog,
|
|
@@ -66,6 +73,28 @@ function fileLocalScope(root, files, config, { changed = false } = {}) {
|
|
|
66
73
|
return scoped;
|
|
67
74
|
}
|
|
68
75
|
|
|
76
|
+
/**
|
|
77
|
+
* `--changed` resolves the touched files plus their forward import closure.
|
|
78
|
+
* The deny-cross-parent pass attributes a finding to the slice file that
|
|
79
|
+
* imports a shared root, so an edit to a shared file alone can complete a new
|
|
80
|
+
* cross-universe path whose importer is outside that closure. When a touched
|
|
81
|
+
* file sits under such a rule's sharedRoots, the graph runs full-tree.
|
|
82
|
+
*/
|
|
83
|
+
function changedSharedRootNeedsFullGraph(scoped, rules) {
|
|
84
|
+
if (!scoped) return false;
|
|
85
|
+
const walls = (rules ?? []).filter(
|
|
86
|
+
(rule) =>
|
|
87
|
+
rule?.allowed === false &&
|
|
88
|
+
rule.peerIsolation === true &&
|
|
89
|
+
sharedImportsSliceMode(rule.sharedImportsSlice) === 'deny-cross-parent'
|
|
90
|
+
);
|
|
91
|
+
if (walls.length === 0) return false;
|
|
92
|
+
for (const file of scoped) {
|
|
93
|
+
if (walls.some((rule) => pathUnderSharedRoot(file, rule.sharedRoots))) return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
69
98
|
function coversGovernedSet(root, scoped, config) {
|
|
70
99
|
const governed = collectGovernedFiles(root, config);
|
|
71
100
|
if (governed.length === 0) return false;
|
|
@@ -100,7 +129,11 @@ function hintCacheKey(root, scopedFiles, arkRules) {
|
|
|
100
129
|
`${rule.sensor ?? ''}\0${rule.mode ?? ''}\0${(rule.appliesTo ?? []).join(',')}`
|
|
101
130
|
)
|
|
102
131
|
.join('\n');
|
|
103
|
-
|
|
132
|
+
// A full-tree key lists every governed file; retaining up to HINT_CACHE_CAP of
|
|
133
|
+
// those raw strings in a long-lived MCP process held megabytes per entry.
|
|
134
|
+
return createHash('sha256')
|
|
135
|
+
.update(`${path.resolve(root)}\0${filesPart}\0${rulesPart}`)
|
|
136
|
+
.digest('hex');
|
|
104
137
|
}
|
|
105
138
|
|
|
106
139
|
function rememberHintCache(key, value) {
|
|
@@ -155,15 +188,17 @@ export function resolveArchitectureSnapshot({
|
|
|
155
188
|
const scoped = fileLocalScope(root, files, effectiveConfig, {
|
|
156
189
|
changed: args?.changed === true,
|
|
157
190
|
});
|
|
191
|
+
// File-local sensors stay on `scoped`; only the graph widens (see helper).
|
|
192
|
+
const graphScope = changedSharedRootNeedsFullGraph(scoped, effectiveConfig.rules) ? null : scoped;
|
|
158
193
|
const facts = resolveCandidateFacts({
|
|
159
194
|
root,
|
|
160
195
|
config: effectiveConfig,
|
|
161
196
|
ts,
|
|
162
197
|
...(args?.tsconfig ? { tsconfig: args.tsconfig } : {}),
|
|
163
198
|
observeInput,
|
|
164
|
-
...(
|
|
165
|
-
? { scopeFiles: [...
|
|
166
|
-
: args?.changed
|
|
199
|
+
...(graphScope
|
|
200
|
+
? { scopeFiles: [...graphScope] }
|
|
201
|
+
: args?.changed && !scoped
|
|
167
202
|
? { scopeFiles: [] }
|
|
168
203
|
: {}),
|
|
169
204
|
});
|
|
@@ -212,9 +247,17 @@ export function resolveArchitectureSnapshot({
|
|
|
212
247
|
const rootsPresent = catalogHasEnforcedInvariant(arkRulesLoad.arkRules?.invariants)
|
|
213
248
|
? declaredCoverageRootsPresent(root, effectiveConfig.coverage)
|
|
214
249
|
: true;
|
|
250
|
+
// Version floor evidence (#338): zero extra I/O unless a floored key is in use.
|
|
251
|
+
const pinEvidence = configVersionFloors(effectiveConfig.rules)
|
|
252
|
+
? {
|
|
253
|
+
arkgatePins: collectArkgatePins(root, { observeInput }),
|
|
254
|
+
runningArkgateVersion: runningArkgateVersion(),
|
|
255
|
+
}
|
|
256
|
+
: {};
|
|
215
257
|
const analyzed = analyzeTrustedResolvedProject({
|
|
216
258
|
contract: analysisContract,
|
|
217
259
|
facts,
|
|
260
|
+
...pinEvidence,
|
|
218
261
|
...(adopted ? { adopted: true } : {}),
|
|
219
262
|
...(adopted && pathPresent === false ? { invariantTestsPathPresent: false } : {}),
|
|
220
263
|
...(rootsPresent === false ? { coverageRootsPresent: false } : {}),
|
|
@@ -229,7 +272,8 @@ export function resolveArchitectureSnapshot({
|
|
|
229
272
|
);
|
|
230
273
|
const result = {
|
|
231
274
|
violations: analyzed.ir.violations,
|
|
232
|
-
|
|
275
|
+
// ADR 0012 D2: an unreferenced arkrules/*.json is advisory drift, never a failure.
|
|
276
|
+
warnings: [...analyzed.ir.warnings, ...arkRulesDriftWarnings(arkRulesLoad.warnings)],
|
|
233
277
|
safety: analyzed.safety,
|
|
234
278
|
parseHealth,
|
|
235
279
|
completeness: analyzed.completeness,
|
|
@@ -13,7 +13,7 @@ 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
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.';
|
|
16
|
+
export const ARKORDER_FIRST_CONTACT_NEXT = 'Next: see the billing gallery at github.com/pedroknigge/arkgate/tree/main/examples/arkorder-billing (git only, not in the npm package), then /ark-adopt to turn the extra on and /ark-order to wire one candidate.';
|
|
17
17
|
function closedMode(value) {
|
|
18
18
|
return value === 'enforced' || value === 'advisory' ? value : null;
|
|
19
19
|
}
|
|
@@ -8,14 +8,106 @@
|
|
|
8
8
|
* Pure CLI helper (bin/lib/ark-order-facts.mjs). Zero Node I/O.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { XI_TTL_KEY_RE } from './ark-order-types.mjs';
|
|
12
|
+
import { sourceHasPersistenceWrite, sourceImportsPersistenceDriverText } from './persistence-write-hint.mjs';
|
|
11
13
|
export const ARKORDER_PLANE_FACTORY = 'createOrderPlane';
|
|
12
14
|
export const ARKORDER_FORBIDDEN_METHODS = ['update', 'patch', 'set', 'mutate'];
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
/**
|
|
16
|
+
* XIWRITE-001: same engine as `globToRegExp` in src/domain/layerMatch.ts.
|
|
17
|
+
* Inlined so generate:cli-pure emits self-contained bin/lib/ark-order-facts.mjs /
|
|
18
|
+
* ark-order-sensors.mjs
|
|
19
|
+
* (layerMatch is derived to bin/ark-layer-match.mjs, not a bin/lib sibling).
|
|
20
|
+
*/
|
|
21
|
+
const appliesToRegexpCache = new Map();
|
|
22
|
+
function escapeAppliesToLiteral(ch) {
|
|
23
|
+
return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
|
|
24
|
+
}
|
|
25
|
+
function normalizeAppliesToGlob(pattern) {
|
|
26
|
+
let out = '';
|
|
27
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
28
|
+
const c = pattern[i];
|
|
29
|
+
if (c === '\\' && i + 1 < pattern.length) {
|
|
30
|
+
const next = pattern[i + 1];
|
|
31
|
+
if ('*?{}[],'.includes(next) || next === '\\') {
|
|
32
|
+
out += '\\' + next;
|
|
33
|
+
i += 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
out += '/';
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
out += c;
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
function appliesToBracesBalanced(glob) {
|
|
44
|
+
let depth = 0;
|
|
45
|
+
for (let i = 0; i < glob.length; i += 1) {
|
|
46
|
+
const c = glob[i];
|
|
47
|
+
if (c === '\\') {
|
|
48
|
+
i += 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (c === '{')
|
|
52
|
+
depth += 1;
|
|
53
|
+
else if (c === '}') {
|
|
54
|
+
depth -= 1;
|
|
55
|
+
if (depth < 0)
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return depth === 0;
|
|
60
|
+
}
|
|
61
|
+
export function arkOrderGlobToRegExp(pattern) {
|
|
62
|
+
const cached = appliesToRegexpCache.get(pattern);
|
|
63
|
+
if (cached)
|
|
64
|
+
return cached;
|
|
65
|
+
const glob = normalizeAppliesToGlob(pattern);
|
|
66
|
+
const useBraces = appliesToBracesBalanced(glob);
|
|
67
|
+
let out = '';
|
|
68
|
+
let braceDepth = 0;
|
|
69
|
+
for (let i = 0; i < glob.length; i += 1) {
|
|
70
|
+
const c = glob[i];
|
|
71
|
+
if (c === '\\' && i + 1 < glob.length) {
|
|
72
|
+
out += escapeAppliesToLiteral(glob[i + 1]);
|
|
73
|
+
i += 1;
|
|
74
|
+
}
|
|
75
|
+
else if (c === '*') {
|
|
76
|
+
if (glob[i + 1] === '*') {
|
|
77
|
+
if (glob[i + 2] === '/') {
|
|
78
|
+
out += '(?:.*/)?';
|
|
79
|
+
i += 2;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
out += '.*';
|
|
83
|
+
i += 1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
out += '[^/]*';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else if (c === '?') {
|
|
91
|
+
out += '[^/]';
|
|
92
|
+
}
|
|
93
|
+
else if (c === '{' && useBraces) {
|
|
94
|
+
out += '(?:';
|
|
95
|
+
braceDepth += 1;
|
|
96
|
+
}
|
|
97
|
+
else if (c === '}' && useBraces && braceDepth > 0) {
|
|
98
|
+
out += ')';
|
|
99
|
+
braceDepth -= 1;
|
|
100
|
+
}
|
|
101
|
+
else if (c === ',' && useBraces && braceDepth > 0) {
|
|
102
|
+
out += '|';
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
out += escapeAppliesToLiteral(c);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const re = new RegExp(`^${out}$`);
|
|
109
|
+
appliesToRegexpCache.set(pattern, re);
|
|
110
|
+
return re;
|
|
19
111
|
}
|
|
20
112
|
function escapeRegExp(value) {
|
|
21
113
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -36,6 +128,45 @@ function stripCommentsPreservingLines(content) {
|
|
|
36
128
|
.replace(/\/\*[\s\S]*?\*\//g, (block) => block.replace(/[^\n]/g, ' '))
|
|
37
129
|
.replace(/(^|[^:])\/\/.*$/gm, (line) => line.replace(/\/\/.*$/, (c) => ' '.repeat(c.length)));
|
|
38
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Specifiers naming `arkgate/order` in one source text (static import / export-from,
|
|
133
|
+
* `require()`, `import()`). Editor-path twin of the resolved dependency facts the
|
|
134
|
+
* kernel-in-domain sensor reads in CI — syntax only, no layer verdict here.
|
|
135
|
+
*/
|
|
136
|
+
export function extractArkOrderModuleImportsFromSource(file, content) {
|
|
137
|
+
const source = stripCommentsPreservingLines(content);
|
|
138
|
+
const out = [];
|
|
139
|
+
const fromRe = /\b(import|export)(\s+type)?\s+(?:[^;]*?\s+from\s*)?['"]([^'"]+)['"]/g;
|
|
140
|
+
let match;
|
|
141
|
+
while ((match = fromRe.exec(source)) !== null) {
|
|
142
|
+
const specifier = match[3] ?? '';
|
|
143
|
+
if (!isArkOrderModuleSpecifier(specifier))
|
|
144
|
+
continue;
|
|
145
|
+
out.push({
|
|
146
|
+
from: file,
|
|
147
|
+
specifier,
|
|
148
|
+
kind: match[1] === 'export' ? 'export' : 'import',
|
|
149
|
+
typeOnly: Boolean(match[2]),
|
|
150
|
+
line: lineAt(content, match.index),
|
|
151
|
+
resolution: 'resolved-external',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const callRe = /\b(require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
155
|
+
while ((match = callRe.exec(source)) !== null) {
|
|
156
|
+
const specifier = match[2] ?? '';
|
|
157
|
+
if (!isArkOrderModuleSpecifier(specifier))
|
|
158
|
+
continue;
|
|
159
|
+
out.push({
|
|
160
|
+
from: file,
|
|
161
|
+
specifier,
|
|
162
|
+
kind: match[1] === 'import' ? 'dynamic-import' : 'require',
|
|
163
|
+
typeOnly: false,
|
|
164
|
+
line: lineAt(content, match.index),
|
|
165
|
+
resolution: 'resolved-external',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
39
170
|
export function extractArkOrderPlaneCallsFromSource(file, content) {
|
|
40
171
|
const source = stripCommentsPreservingLines(content);
|
|
41
172
|
const facts = [];
|
|
@@ -46,19 +177,190 @@ export function extractArkOrderPlaneCallsFromSource(file, content) {
|
|
|
46
177
|
}
|
|
47
178
|
return facts;
|
|
48
179
|
}
|
|
49
|
-
|
|
180
|
+
const PLANE_BINDING_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=;\n]+)?=\s*createOrderPlane\s*(?:<[^>]*>)?\s*\(/g;
|
|
181
|
+
/**
|
|
182
|
+
* Conventional plane names: `plane`, `orderPlane`, `billingPlane`, … (not the factory).
|
|
183
|
+
* A name alone is never evidence (`clipPlane.set`, `controlPlane.update` are not ArkOrder):
|
|
184
|
+
* it counts only in a file that imports `arkgate/order`, or on a named import from a
|
|
185
|
+
* declared `planeRoots` module.
|
|
186
|
+
*/
|
|
187
|
+
const CONVENTIONAL_PLANE_NAME_RE = /^(?:plane|[A-Za-z_$][\w$]*Plane)$/;
|
|
188
|
+
/** `plane: OrderPlane` — parameters, fields, and variables annotated with the plane type. */
|
|
189
|
+
const ORDER_PLANE_ANNOTATION_RE = /\b([A-Za-z_$][\w$]*)\s*[?!]?\s*:\s*(?:Readonly\s*<\s*)?OrderPlane\b/g;
|
|
190
|
+
const ARKORDER_IMPORT_RE = /\bfrom\s+['"]arkgate\/order(?:\/[^'"]*)?['"]|\brequire\s*\(\s*['"]arkgate\/order(?:\/[^'"]*)?['"]|\bimport\s*\(\s*['"]arkgate\/order(?:\/[^'"]*)?['"]/;
|
|
191
|
+
const NAMED_IMPORT_RE = /\bimport\s+(?:type\s+)?\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g;
|
|
192
|
+
const RELATIVE_EXTENSIONS = ['', '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
|
|
193
|
+
/** Trailing identifier, allowing a non-null `!` and optional-chain `?` before the dot. */
|
|
194
|
+
const TRAILING_IDENTIFIER_RE = /([A-Za-z_$][\w$]*)\s*!?\s*\??\s*$/;
|
|
195
|
+
/** Leading identifier inside a parenthesized receiver: `x as T`, `<T>x`, `x!`, `x`. */
|
|
196
|
+
const WRAPPED_IDENTIFIER_RE = /^\s*(?:<[^<>]*>\s*)?([A-Za-z_$][\w$]*)\s*!?\s*(?:\b(?:as|satisfies)\b[\s\S]*)?$/;
|
|
197
|
+
/** Identifiers bound to `createOrderPlane(...)` in this file. */
|
|
198
|
+
export function arkOrderPlaneBindings(content) {
|
|
50
199
|
const source = stripCommentsPreservingLines(content);
|
|
200
|
+
const names = new Set();
|
|
201
|
+
const re = new RegExp(PLANE_BINDING_RE.source, 'g');
|
|
202
|
+
let match;
|
|
203
|
+
while ((match = re.exec(source)) !== null) {
|
|
204
|
+
if (match[1])
|
|
205
|
+
names.add(match[1]);
|
|
206
|
+
}
|
|
207
|
+
return [...names].sort();
|
|
208
|
+
}
|
|
209
|
+
function normalizeRelativePath(parts) {
|
|
210
|
+
const out = [];
|
|
211
|
+
for (const part of parts) {
|
|
212
|
+
if (part === '' || part === '.')
|
|
213
|
+
continue;
|
|
214
|
+
if (part === '..') {
|
|
215
|
+
if (out.length === 0)
|
|
216
|
+
return '';
|
|
217
|
+
out.pop();
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
out.push(part);
|
|
221
|
+
}
|
|
222
|
+
return out.join('/');
|
|
223
|
+
}
|
|
224
|
+
/** Project-relative candidates for a relative specifier (lexical; no tsconfig paths). */
|
|
225
|
+
function relativeModuleCandidates(file, specifier) {
|
|
226
|
+
if (!specifier.startsWith('./') && !specifier.startsWith('../'))
|
|
227
|
+
return [];
|
|
228
|
+
const dir = file.replace(/\\/g, '/').split('/').slice(0, -1);
|
|
229
|
+
const base = normalizeRelativePath([...dir, ...specifier.split('/')]);
|
|
230
|
+
if (!base)
|
|
231
|
+
return [];
|
|
232
|
+
const stem = base.replace(/\.(?:[cm]?js|jsx)$/, '');
|
|
233
|
+
const out = new Set();
|
|
234
|
+
for (const root of [base, stem]) {
|
|
235
|
+
for (const ext of RELATIVE_EXTENSIONS)
|
|
236
|
+
out.add(`${root}${ext}`);
|
|
237
|
+
for (const ext of RELATIVE_EXTENSIONS.slice(1))
|
|
238
|
+
out.add(`${root}/index${ext}`);
|
|
239
|
+
}
|
|
240
|
+
return [...out];
|
|
241
|
+
}
|
|
242
|
+
function specifierResolvesToPlaneRoot(file, specifier, planeRoots) {
|
|
243
|
+
const candidates = relativeModuleCandidates(file, specifier);
|
|
244
|
+
if (candidates.length === 0)
|
|
245
|
+
return false;
|
|
246
|
+
return planeRoots.some((pattern) => {
|
|
247
|
+
let re;
|
|
248
|
+
try {
|
|
249
|
+
re = arkOrderGlobToRegExp(pattern);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
return candidates.some((candidate) => re.test(candidate));
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Identifiers that name an order plane in this file, from direct evidence only:
|
|
259
|
+
* bound to `createOrderPlane(...)`, annotated `: OrderPlane`, or a `plane` / `*Plane`
|
|
260
|
+
* named import from a declared plane root. `isArkOrderFile` reports an `arkgate/order`
|
|
261
|
+
* import, which admits the conventional names file-wide.
|
|
262
|
+
*/
|
|
263
|
+
export function arkOrderPlaneReceivers(file, content, options = {}) {
|
|
264
|
+
const source = stripCommentsPreservingLines(content);
|
|
265
|
+
const names = new Set(arkOrderPlaneBindings(content));
|
|
266
|
+
const annotated = new RegExp(ORDER_PLANE_ANNOTATION_RE.source, 'g');
|
|
267
|
+
let match;
|
|
268
|
+
while ((match = annotated.exec(source)) !== null) {
|
|
269
|
+
if (match[1])
|
|
270
|
+
names.add(match[1]);
|
|
271
|
+
}
|
|
272
|
+
const planeRoots = options.planeRoots ?? [];
|
|
273
|
+
if (planeRoots.length > 0) {
|
|
274
|
+
const imports = new RegExp(NAMED_IMPORT_RE.source, 'g');
|
|
275
|
+
while ((match = imports.exec(source)) !== null) {
|
|
276
|
+
if (!specifierResolvesToPlaneRoot(file, match[2] ?? '', planeRoots))
|
|
277
|
+
continue;
|
|
278
|
+
for (const element of (match[1] ?? '').split(',')) {
|
|
279
|
+
const parts = element
|
|
280
|
+
.trim()
|
|
281
|
+
.replace(/^type\s+/, '')
|
|
282
|
+
.split(/\s+as\s+/);
|
|
283
|
+
const imported = parts[0]?.trim() ?? '';
|
|
284
|
+
const local = (parts[1] ?? parts[0] ?? '').trim();
|
|
285
|
+
if (!local || !/^[A-Za-z_$][\w$]*$/.test(local))
|
|
286
|
+
continue;
|
|
287
|
+
if (CONVENTIONAL_PLANE_NAME_RE.test(imported) || CONVENTIONAL_PLANE_NAME_RE.test(local)) {
|
|
288
|
+
names.add(local);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
names.delete(ARKORDER_PLANE_FACTORY);
|
|
294
|
+
return { names: [...names].sort(), isArkOrderFile: ARKORDER_IMPORT_RE.test(source) };
|
|
295
|
+
}
|
|
296
|
+
const KEYWORDS_BEFORE_PAREN = new Set([
|
|
297
|
+
'return', 'await', 'void', 'typeof', 'yield', 'in', 'of', 'case', 'else', 'do', 'throw', 'delete',
|
|
298
|
+
]);
|
|
299
|
+
/** True when `(` at this point opens call arguments (`f(x).update(`), not a wrapper. */
|
|
300
|
+
function isCallBefore(prefix) {
|
|
301
|
+
const tail = prefix.replace(/\s+$/, '');
|
|
302
|
+
if (/[)\]]$/.test(tail))
|
|
303
|
+
return true;
|
|
304
|
+
const word = /([A-Za-z_$][\w$]*)$/.exec(tail);
|
|
305
|
+
return word !== null && !KEYWORDS_BEFORE_PAREN.has(word[1]);
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Receiver identifier right before a `.method(` — unwraps `?.`, `!`, and one balanced
|
|
309
|
+
* `( x as T )` / `( x satisfies T )` / `(<T>x)` / `(x)` wrapper (T may itself contain
|
|
310
|
+
* parentheses). `this.plane` yields `plane`.
|
|
311
|
+
*/
|
|
312
|
+
function receiverBefore(before) {
|
|
313
|
+
const trimmed = before.replace(/\s*!?\s*\??\s*$/, '');
|
|
314
|
+
if (!trimmed.endsWith(')')) {
|
|
315
|
+
const match = TRAILING_IDENTIFIER_RE.exec(trimmed);
|
|
316
|
+
return match ? match[1] ?? null : null;
|
|
317
|
+
}
|
|
318
|
+
let depth = 0;
|
|
319
|
+
for (let index = trimmed.length - 1; index >= 0; index -= 1) {
|
|
320
|
+
const ch = trimmed[index];
|
|
321
|
+
if (ch === ')')
|
|
322
|
+
depth += 1;
|
|
323
|
+
else if (ch === '(') {
|
|
324
|
+
depth -= 1;
|
|
325
|
+
if (depth === 0) {
|
|
326
|
+
if (isCallBefore(trimmed.slice(0, index)))
|
|
327
|
+
return null;
|
|
328
|
+
const inner = trimmed.slice(index + 1, trimmed.length - 1);
|
|
329
|
+
const match = WRAPPED_IDENTIFIER_RE.exec(inner);
|
|
330
|
+
return match ? match[1] ?? null : null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
function planeReceiverContext(file, content, options) {
|
|
337
|
+
const resolved = arkOrderPlaneReceivers(file, content, options);
|
|
338
|
+
return { names: new Set(resolved.names), isArkOrderFile: resolved.isArkOrderFile };
|
|
339
|
+
}
|
|
340
|
+
function isPlaneReceiver(name, context) {
|
|
341
|
+
if (!name || name === ARKORDER_PLANE_FACTORY)
|
|
342
|
+
return false;
|
|
343
|
+
if (context.names.has(name))
|
|
344
|
+
return true;
|
|
345
|
+
return context.isArkOrderFile && CONVENTIONAL_PLANE_NAME_RE.test(name);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Direct evidence only: `.update|patch|set|mutate(` whose receiver is a plane (see
|
|
349
|
+
* arkOrderPlaneReceivers). EOSF5-001: Map / URLSearchParams / React `order.set` /
|
|
350
|
+
* `prisma.x.update` are not ξ mutation, even inside the plane-root file; nor is
|
|
351
|
+
* `clipPlane.set` / `controlPlane.update` in a file with no ArkOrder evidence.
|
|
352
|
+
*/
|
|
353
|
+
export function extractArkOrderGenericUpdatesFromSource(file, content, options = {}) {
|
|
354
|
+
const source = stripCommentsPreservingLines(content);
|
|
355
|
+
const context = planeReceiverContext(file, content, options);
|
|
51
356
|
const facts = [];
|
|
52
357
|
const re = /\.((?:update|patch|set|mutate))\s*\(/g;
|
|
53
358
|
let match;
|
|
54
359
|
while ((match = re.exec(source)) !== null) {
|
|
55
360
|
const method = match[1];
|
|
56
|
-
const before = source.slice(Math.max(0, match.index -
|
|
57
|
-
|
|
58
|
-
const calleeIsOrderPlane = /\b(?:plane|orderPlane)\s*(?:\?|!)?$/.test(before);
|
|
59
|
-
if (!calleeIsOrderPlane && !/\bcreateOrderPlane\b/.test(source)) {
|
|
361
|
+
const before = source.slice(Math.max(0, match.index - 160), match.index);
|
|
362
|
+
if (!isPlaneReceiver(receiverBefore(before), context))
|
|
60
363
|
continue;
|
|
61
|
-
}
|
|
62
364
|
facts.push({ file, line: lineAt(content, match.index), method });
|
|
63
365
|
}
|
|
64
366
|
return facts;
|
|
@@ -71,7 +373,7 @@ export function extractArkOrderXiFieldWritesFromSource(file, content, xiKeys) {
|
|
|
71
373
|
if (xiKeys.length === 0)
|
|
72
374
|
return [];
|
|
73
375
|
const source = stripCommentsPreservingLines(content);
|
|
74
|
-
if (!
|
|
376
|
+
if (!sourceImportsPersistenceDriverText(source) || !sourceHasPersistenceWrite(source))
|
|
75
377
|
return [];
|
|
76
378
|
const facts = [];
|
|
77
379
|
const seen = new Set();
|
|
@@ -91,11 +393,24 @@ export function extractArkOrderXiFieldWritesFromSource(file, content, xiKeys) {
|
|
|
91
393
|
}
|
|
92
394
|
return facts;
|
|
93
395
|
}
|
|
94
|
-
/**
|
|
396
|
+
/**
|
|
397
|
+
* Holder names that read as a Release or ξ store: whole-word `xi` / `release` /
|
|
398
|
+
* `pattern` / `house` / `current`; an `xi` camelCase head (`xiNext`, `xiState`); or a
|
|
399
|
+
* `…Xi` / `…Release` / `…Pattern` camelCase tail (`nextXi`, `currentRelease`).
|
|
400
|
+
* Trade-off (ADR 0013, prefer false negatives): names that only *start* with
|
|
401
|
+
* release/current/pattern (`releaseState`, `currentResidual`, `patternResult`,
|
|
402
|
+
* `releaseDate`) are not evidence, so `this.releaseState = plane.ingest(e)` stays silent.
|
|
403
|
+
*/
|
|
404
|
+
const INGEST_XI_HOLDER_SOURCE = '(?:xi(?:[A-Z][\\w$]*)?|release|pattern|house|current|[A-Za-z_$][\\w$]*?(?:Xi|Release|Pattern))(?![\\w$])';
|
|
405
|
+
/**
|
|
406
|
+
* ingest() assigned into a Release / ξ holder (INGEST_XI_HOLDER_SOURCE), as a binding
|
|
407
|
+
* or a property write such as `store.xi =` / `release.xi[k] =` / `this.nextXi =`.
|
|
408
|
+
* Comparisons (`===`) and arrows (`=>`) are not evidence.
|
|
409
|
+
*/
|
|
95
410
|
export function extractArkOrderIngestWritesXiFromSource(file, content) {
|
|
96
411
|
const source = stripCommentsPreservingLines(content);
|
|
97
412
|
const facts = [];
|
|
98
|
-
const re =
|
|
413
|
+
const re = new RegExp(`(?:(?<![\\w$])|\\.)${INGEST_XI_HOLDER_SOURCE}(?:\\s*:\\s*[A-Za-z_$][\\w$.<>, |[\\]]*?)?\\s*(?:\\[[^\\]\\n]*\\])?\\s*=(?![=>])\\s*[^\\n;]{0,160}?\\bingest\\s*\\(`, 'g');
|
|
99
414
|
let match;
|
|
100
415
|
while ((match = re.exec(source)) !== null) {
|
|
101
416
|
facts.push({ file, line: lineAt(content, match.index) });
|
|
@@ -117,3 +432,83 @@ export function extractArkOrderReleaseKeyCountsFromSource(file, content) {
|
|
|
117
432
|
}
|
|
118
433
|
return facts;
|
|
119
434
|
}
|
|
435
|
+
/** Keys of a flat object-literal body: `key: v`, `'key': v`, and shorthand `key`. */
|
|
436
|
+
function literalKeys(body) {
|
|
437
|
+
const keys = [];
|
|
438
|
+
for (const entry of body.split(',')) {
|
|
439
|
+
const match = /^\s*(['"]?)([A-Za-z_$][\w$]*)\1\s*(?::|$)/.exec(entry);
|
|
440
|
+
if (match?.[2])
|
|
441
|
+
keys.push(match[2]);
|
|
442
|
+
}
|
|
443
|
+
return keys;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Freshness keys in the ξ literal (first argument) of `plane.release({...})` /
|
|
447
|
+
* `plane.proposeRelease({...})`. σ (the second argument) may carry freshUntil — that
|
|
448
|
+
* is where freshness belongs. The receiver must be a plane (arkOrderPlaneReceivers), so
|
|
449
|
+
* a lock/lease `release({ ttl })` is never evidence. Shorthand `{ plan, ttl }` counts.
|
|
450
|
+
*/
|
|
451
|
+
export function extractArkOrderXiTtlKeysFromSource(file, content, options = {}) {
|
|
452
|
+
const source = stripCommentsPreservingLines(content);
|
|
453
|
+
const context = planeReceiverContext(file, content, options);
|
|
454
|
+
const facts = [];
|
|
455
|
+
const re = /\.(?:release|proposeRelease)\s*\(\s*\{([^}]*)\}/g;
|
|
456
|
+
let match;
|
|
457
|
+
while ((match = re.exec(source)) !== null) {
|
|
458
|
+
const before = source.slice(Math.max(0, match.index - 160), match.index);
|
|
459
|
+
if (!isPlaneReceiver(receiverBefore(before), context))
|
|
460
|
+
continue;
|
|
461
|
+
for (const key of literalKeys(match[1] ?? '')) {
|
|
462
|
+
if (!XI_TTL_KEY_RE.test(key))
|
|
463
|
+
continue;
|
|
464
|
+
facts.push({ file, line: lineAt(content, match.index), key });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return facts;
|
|
468
|
+
}
|
|
469
|
+
function stringLiterals(body) {
|
|
470
|
+
const out = [];
|
|
471
|
+
const re = /(['"`])([^'"`\n\\]+)\1/g;
|
|
472
|
+
let match;
|
|
473
|
+
while ((match = re.exec(body)) !== null) {
|
|
474
|
+
if (match[2])
|
|
475
|
+
out.push(match[2]);
|
|
476
|
+
}
|
|
477
|
+
return out;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Direct evidence only: in a file that calls createOrderPlane, a literal
|
|
481
|
+
* `allowedKinds: [...]` entry that a literal `cannotObserve: [...]` denies. Computed
|
|
482
|
+
* or imported arrays stay silent (inference never blocks) — the runtime plane still
|
|
483
|
+
* throws ARKORDER_INFORMATION_BUDGET before such a Release is persisted.
|
|
484
|
+
*/
|
|
485
|
+
export function extractArkOrderBudgetLeaksFromSource(file, content) {
|
|
486
|
+
const source = stripCommentsPreservingLines(content);
|
|
487
|
+
if (!/\bcreateOrderPlane\b/.test(source))
|
|
488
|
+
return [];
|
|
489
|
+
const denied = new Set();
|
|
490
|
+
const deniedRe = /\bcannotObserve\s*:\s*\[([^\]]*)\]/g;
|
|
491
|
+
let match;
|
|
492
|
+
while ((match = deniedRe.exec(source)) !== null) {
|
|
493
|
+
for (const kind of stringLiterals(match[1] ?? ''))
|
|
494
|
+
denied.add(kind);
|
|
495
|
+
}
|
|
496
|
+
if (denied.size === 0)
|
|
497
|
+
return [];
|
|
498
|
+
const facts = [];
|
|
499
|
+
const seen = new Set();
|
|
500
|
+
const allowedRe = /\ballowedKinds\s*:\s*\[([^\]]*)\]/g;
|
|
501
|
+
while ((match = allowedRe.exec(source)) !== null) {
|
|
502
|
+
const line = lineAt(content, match.index);
|
|
503
|
+
for (const kind of stringLiterals(match[1] ?? '')) {
|
|
504
|
+
if (!denied.has(kind))
|
|
505
|
+
continue;
|
|
506
|
+
const stamp = `${line}:${kind}`;
|
|
507
|
+
if (seen.has(stamp))
|
|
508
|
+
continue;
|
|
509
|
+
seen.add(stamp);
|
|
510
|
+
facts.push({ file, line, kind });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return facts;
|
|
514
|
+
}
|