arkgate 4.8.23 → 4.8.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +153 -0
- package/README.md +7 -5
- package/bin/ark-check-runtime.mjs +41 -91
- package/bin/ark-dashboard.mjs +56 -53
- package/bin/ark-layer-match.mjs +707 -121
- package/bin/ark-mcp-runtime.mjs +448 -169
- package/bin/ark-mcp.mjs +68 -4
- package/bin/ark-shared.mjs +42 -22
- package/bin/ark.mjs +65 -97
- package/bin/lib/adapter-contract-types.mjs +5 -0
- package/bin/lib/adapter-contract.mjs +15 -0
- package/bin/lib/adoption-stance.mjs +2 -2
- package/bin/lib/adr-path.mjs +3 -3
- package/bin/lib/agent-gates.mjs +0 -61
- package/bin/lib/agent-homes.mjs +12 -21
- package/bin/lib/agent-projection-command.mjs +9 -5
- package/bin/lib/ambient-state.mjs +9 -30
- package/bin/lib/analysis-engine.mjs +15 -11
- package/bin/lib/architecture-scan.mjs +61 -15
- package/bin/lib/ark-order-doctor.mjs +4 -29
- package/bin/lib/ark-order-facts.mjs +319 -17
- package/bin/lib/ark-order-sensors.mjs +28 -101
- package/bin/lib/ark-order-types.mjs +3 -2
- package/bin/lib/ark-run-doctor.mjs +17 -28
- package/bin/lib/ark-run-facts.mjs +461 -48
- package/bin/lib/ark-run-sensors.mjs +6 -4
- package/bin/lib/arkgate-pins.mjs +91 -0
- package/bin/lib/arkrule-file-hints.mjs +2 -47
- package/bin/lib/arkrules-contract.mjs +124 -133
- package/bin/lib/arkrules-sensors.mjs +55 -252
- package/bin/lib/check-args.mjs +85 -1
- package/bin/lib/ci-and-commands.mjs +24 -78
- package/bin/lib/ci-merge-boundary.mjs +1 -1
- package/bin/lib/class-source-scan.mjs +479 -0
- package/bin/lib/clone-detection.mjs +474 -0
- package/bin/lib/codex-home.mjs +5 -3
- package/bin/lib/config-contract-slices.mjs +739 -0
- package/bin/lib/config-contract.mjs +42 -607
- package/bin/lib/config-extras.mjs +16 -22
- package/bin/lib/config-warnings.mjs +32 -0
- package/bin/lib/contract-smells.mjs +33 -15
- package/bin/lib/core-ratchet.mjs +0 -2
- package/bin/lib/dashboard-cli.mjs +62 -0
- package/bin/lib/deep-module-coach.mjs +2 -4
- package/bin/lib/deploy-path.mjs +13 -38
- package/bin/lib/design-delta.mjs +1 -1
- package/bin/lib/design-smells.mjs +2 -2
- package/bin/lib/diagnostic-catalog.mjs +23 -9
- package/bin/lib/doctor-advisories.mjs +78 -8
- package/bin/lib/doctor-green-cite.mjs +1 -1
- package/bin/lib/doctor-human.mjs +1 -1
- package/bin/lib/doctor-next-actions.mjs +19 -4
- package/bin/lib/doctor-plan.mjs +9 -13
- package/bin/lib/duplication-io.mjs +572 -0
- package/bin/lib/effective-contract-load.mjs +282 -83
- package/bin/lib/enforcement-profiles.mjs +40 -4
- package/bin/lib/entry-points-io.mjs +424 -0
- package/bin/lib/extra-merge-teeth.mjs +36 -0
- package/bin/lib/field-install.mjs +38 -14
- package/bin/lib/first-run-help.mjs +10 -5
- package/bin/lib/flat-parent-importers.mjs +139 -0
- package/bin/lib/flat-parent-pilot.mjs +40 -100
- package/bin/lib/gate-files.mjs +58 -90
- package/bin/lib/git-change-scope.mjs +109 -0
- package/bin/lib/github-enforcement.mjs +166 -29
- package/bin/lib/golden-pattern.mjs +1 -1
- package/bin/lib/hook-templates.mjs +17 -15
- package/bin/lib/host-support-matrix.mjs +3 -1
- package/bin/lib/html-report-advisories.mjs +4 -0
- package/bin/lib/html-report-depth.mjs +2 -2
- package/bin/lib/html-report.mjs +2 -2
- package/bin/lib/import-graph-projection.mjs +112 -0
- package/bin/lib/import-resolve.mjs +13 -1
- package/bin/lib/improvement-compass-doctor.mjs +1 -24
- package/bin/lib/install-migrate.mjs +50 -73
- package/bin/lib/invariant-coverage-io.mjs +4 -3
- package/bin/lib/invariant-coverage.mjs +85 -11
- package/bin/lib/invariant-probe-cli.mjs +347 -0
- package/bin/lib/invariant-probe-io.mjs +165 -0
- package/bin/lib/invariant-probe-runner.mjs +268 -0
- package/bin/lib/invariant-probe-sites.mjs +174 -0
- package/bin/lib/invariant-probe-workspace.mjs +183 -0
- package/bin/lib/invariant-probe.mjs +461 -0
- package/bin/lib/invariant-tests-path.mjs +16 -22
- package/bin/lib/literal-path-drift.mjs +4 -2
- package/bin/lib/managed-upgrade.mjs +2 -8
- package/bin/lib/mcp-adoption.mjs +13 -6
- package/bin/lib/mcp-hook-payload.mjs +403 -63
- package/bin/lib/orphan-modules-io.mjs +458 -0
- package/bin/lib/orphan-modules.mjs +251 -0
- package/bin/lib/outside-importers.mjs +197 -0
- package/bin/lib/package-manager.mjs +60 -2
- package/bin/lib/parse-health.mjs +1 -1
- package/bin/lib/passthrough-commands.mjs +83 -0
- package/bin/lib/persistence-write-hint.mjs +106 -0
- package/bin/lib/pin-dependent-gates.mjs +117 -0
- package/bin/lib/policy-delta-io.mjs +219 -14
- package/bin/lib/post-green-path.mjs +1 -1
- package/bin/lib/prepare-change.mjs +115 -7
- package/bin/lib/prepare-write.mjs +2 -2
- package/bin/lib/presets.mjs +59 -73
- package/bin/lib/product-copy.mjs +1 -6
- package/bin/lib/project-root.mjs +31 -62
- package/bin/lib/remediation.mjs +23 -4
- package/bin/lib/resolved-candidate-facts.mjs +405 -117
- package/bin/lib/rules-inventory.mjs +22 -3
- package/bin/lib/rules-under-contract.mjs +247 -9
- package/bin/lib/scan-files.mjs +36 -4
- package/bin/lib/schema-validation.mjs +130 -0
- package/bin/lib/sensor-promote-cli.mjs +2 -11
- package/bin/lib/sensor-promote-io.mjs +33 -22
- package/bin/lib/sensor-promotion.mjs +1 -1
- package/bin/lib/skill-catalog-apply.mjs +1 -1
- package/bin/lib/skill-install.mjs +42 -38
- package/bin/lib/snippet-analysis.mjs +140 -6
- package/bin/lib/source-policy.mjs +34 -0
- package/bin/lib/start-install-recovery.mjs +72 -2
- package/bin/lib/start-preview.mjs +31 -1
- package/bin/lib/states-transitions-presence.mjs +3 -3
- package/bin/lib/status-command.mjs +35 -22
- package/bin/lib/status-manifest.mjs +50 -30
- package/bin/lib/status-transition-catalog.mjs +0 -6
- package/bin/lib/suggestions.mjs +3 -3
- package/bin/lib/team-parliament-io.mjs +76 -88
- package/bin/lib/team-parliament.mjs +21 -10
- package/bin/lib/upgrade-behavior-changes.mjs +2 -2
- package/bin/lib/upgrade-command.mjs +2 -24
- package/bin/lib/upgrade-outcome.mjs +1 -1
- package/bin/lib/upstream-report.mjs +21 -10
- package/bin/lib/violations.mjs +10 -9
- package/bin/lib/weakest-link.mjs +13 -0
- package/bin/lib/write-path-capabilities.mjs +6 -4
- package/dist/chunk-A5EIY3HI.cjs +5 -0
- package/dist/chunk-AHACEKSV.js +16 -0
- package/dist/chunk-SVSQVGFJ.js +5 -0
- package/dist/chunk-UN5EH4QV.cjs +16 -0
- package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-GPvvdleM.d.cts} +30 -4
- package/dist/configTypes-GPvvdleM.d.ts +257 -0
- package/dist/eslint/index.cjs +18 -5
- package/dist/eslint/index.d.cts +368 -0
- package/dist/eslint/index.d.ts +139 -156
- package/dist/eslint/index.js +11 -5
- package/dist/index.cjs +36 -35
- package/dist/index.d.cts +5547 -0
- package/dist/index.d.ts +3283 -198
- package/dist/index.js +36 -35
- package/dist/inspectorListen-6DQVJ4DC.js +1 -0
- package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
- package/dist/nestjs/index.cjs +1 -18
- package/dist/nestjs/index.d.cts +22 -0
- package/dist/nestjs/index.d.ts +1 -3
- package/dist/nestjs/index.js +1 -18
- package/dist/order/index.cjs +1 -1
- package/dist/order/index.d.cts +155 -0
- package/dist/order/index.d.ts +15 -3
- package/dist/order/index.js +1 -1
- package/dist/runtime/index.cjs +13 -30
- package/dist/{diagnosticCatalog-35mrwAHe.d.ts → runtime/index.d.cts} +828 -276
- package/dist/runtime/index.d.ts +2741 -133
- package/dist/runtime/index.js +13 -30
- package/dist/{types-Bdj32QwA.d.ts → types-CnauMQQL.d.cts} +607 -163
- package/dist/types-CnauMQQL.d.ts +1800 -0
- package/docs/README.md +3 -1
- package/docs/agent-guide.md +217 -16
- package/docs/ai-gates.md +174 -26
- package/docs/arkorder.md +32 -15
- package/docs/brownfield-adoption.md +5 -1
- package/docs/configuration.md +230 -31
- package/docs/develop.md +2 -1
- package/docs/diagnostics.md +212 -19
- package/docs/package-surface.md +35 -9
- package/docs/product-voice.md +7 -1
- package/package.json +69 -19
- package/schemas/ark.analysis-result.schema.json +15 -0
- package/schemas/ark.config.schema.json +77 -12
- package/schemas/ark.invariant-probe.schema.json +368 -0
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
- package/schemas/ark.status-manifest.schema.json +22 -0
- package/server.json +3 -9
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +5 -1
- package/templates/agent-skills/ark-contract/SKILL.md +17 -0
- package/templates/agent-skills/ark-coverage/SKILL.md +2 -0
- package/templates/agent-skills/ark-explore/SKILL.md +7 -0
- package/templates/agent-skills/ark-fix/SKILL.md +20 -0
- package/templates/agent-skills/ark-order/SKILL.md +4 -2
- package/templates/agent-skills/ark-place/SKILL.md +8 -0
- package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
- package/templates/arkrules/PersistenceAdapters.json +1 -1
- package/templates/arkrules/PresentationAdapters.json +1 -1
- package/templates/skills/ark-adopt.md +5 -1
- package/templates/skills/ark-contract.md +17 -0
- package/templates/skills/ark-coverage.md +2 -0
- package/templates/skills/ark-explore.md +7 -0
- package/templates/skills/ark-fix.md +20 -0
- package/templates/skills/ark-order.md +4 -2
- package/templates/skills/ark-place.md +8 -0
- package/templates/skills/ark-runtime.md +6 -1
- package/bin/lib/ark-order-error.mjs +0 -18
- package/bin/lib/ark-order-invariants.mjs +0 -346
- package/dist/types-Ik_OakcI.d.ts +0 -183
|
@@ -2,15 +2,23 @@
|
|
|
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
|
+
arkRulesLoadFailed,
|
|
20
|
+
loadEffectiveArkRulesFromDisk,
|
|
21
|
+
} from './effective-contract-load.mjs';
|
|
14
22
|
import {
|
|
15
23
|
coverageOptionsFromConfig,
|
|
16
24
|
invariantIdsFromCatalog,
|
|
@@ -24,6 +32,7 @@ import {
|
|
|
24
32
|
} from './invariant-tests-path.mjs';
|
|
25
33
|
import { loadArkRuleFileHints } from './arkrule-file-hints.mjs';
|
|
26
34
|
import { collectGovernedFiles } from './scan-files.mjs';
|
|
35
|
+
import { projectImporterIndex } from './import-graph-projection.mjs';
|
|
27
36
|
|
|
28
37
|
const HINT_CACHE_CAP = 16;
|
|
29
38
|
/** Process-local hint map keyed by scoped path + content hash. Not a second engine. */
|
|
@@ -66,6 +75,28 @@ function fileLocalScope(root, files, config, { changed = false } = {}) {
|
|
|
66
75
|
return scoped;
|
|
67
76
|
}
|
|
68
77
|
|
|
78
|
+
/**
|
|
79
|
+
* `--changed` resolves the touched files plus their forward import closure.
|
|
80
|
+
* The deny-cross-parent pass attributes a finding to the slice file that
|
|
81
|
+
* imports a shared root, so an edit to a shared file alone can complete a new
|
|
82
|
+
* cross-universe path whose importer is outside that closure. When a touched
|
|
83
|
+
* file sits under such a rule's sharedRoots, the graph runs full-tree.
|
|
84
|
+
*/
|
|
85
|
+
function changedSharedRootNeedsFullGraph(scoped, rules) {
|
|
86
|
+
if (!scoped) return false;
|
|
87
|
+
const walls = (rules ?? []).filter(
|
|
88
|
+
(rule) =>
|
|
89
|
+
rule?.allowed === false &&
|
|
90
|
+
rule.peerIsolation === true &&
|
|
91
|
+
sharedImportsSliceMode(rule.sharedImportsSlice) === 'deny-cross-parent'
|
|
92
|
+
);
|
|
93
|
+
if (walls.length === 0) return false;
|
|
94
|
+
for (const file of scoped) {
|
|
95
|
+
if (walls.some((rule) => pathUnderSharedRoot(file, rule.sharedRoots))) return true;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
69
100
|
function coversGovernedSet(root, scoped, config) {
|
|
70
101
|
const governed = collectGovernedFiles(root, config);
|
|
71
102
|
if (governed.length === 0) return false;
|
|
@@ -100,7 +131,11 @@ function hintCacheKey(root, scopedFiles, arkRules) {
|
|
|
100
131
|
`${rule.sensor ?? ''}\0${rule.mode ?? ''}\0${(rule.appliesTo ?? []).join(',')}`
|
|
101
132
|
)
|
|
102
133
|
.join('\n');
|
|
103
|
-
|
|
134
|
+
// A full-tree key lists every governed file; retaining up to HINT_CACHE_CAP of
|
|
135
|
+
// those raw strings in a long-lived MCP process held megabytes per entry.
|
|
136
|
+
return createHash('sha256')
|
|
137
|
+
.update(`${path.resolve(root)}\0${filesPart}\0${rulesPart}`)
|
|
138
|
+
.digest('hex');
|
|
104
139
|
}
|
|
105
140
|
|
|
106
141
|
function rememberHintCache(key, value) {
|
|
@@ -155,15 +190,17 @@ export function resolveArchitectureSnapshot({
|
|
|
155
190
|
const scoped = fileLocalScope(root, files, effectiveConfig, {
|
|
156
191
|
changed: args?.changed === true,
|
|
157
192
|
});
|
|
193
|
+
// File-local sensors stay on `scoped`; only the graph widens (see helper).
|
|
194
|
+
const graphScope = changedSharedRootNeedsFullGraph(scoped, effectiveConfig.rules) ? null : scoped;
|
|
158
195
|
const facts = resolveCandidateFacts({
|
|
159
196
|
root,
|
|
160
197
|
config: effectiveConfig,
|
|
161
198
|
ts,
|
|
162
199
|
...(args?.tsconfig ? { tsconfig: args.tsconfig } : {}),
|
|
163
200
|
observeInput,
|
|
164
|
-
...(
|
|
165
|
-
? { scopeFiles: [...
|
|
166
|
-
: args?.changed
|
|
201
|
+
...(graphScope
|
|
202
|
+
? { scopeFiles: [...graphScope] }
|
|
203
|
+
: args?.changed && !scoped
|
|
167
204
|
? { scopeFiles: [] }
|
|
168
205
|
: {}),
|
|
169
206
|
});
|
|
@@ -171,13 +208,7 @@ export function resolveArchitectureSnapshot({
|
|
|
171
208
|
observeInput,
|
|
172
209
|
});
|
|
173
210
|
if (arkRulesLoad.errors.length > 0) {
|
|
174
|
-
|
|
175
|
-
.map((issue) => `- ${issue.path}: ${issue.message}`)
|
|
176
|
-
.join('\n');
|
|
177
|
-
const err = new Error(`Invalid Effective Contract (${configPath}):\n${message}`);
|
|
178
|
-
err.code = 'ARKRULES_LOAD_FAILED';
|
|
179
|
-
err.issues = arkRulesLoad.errors;
|
|
180
|
-
throw err;
|
|
211
|
+
throw arkRulesLoadFailed(configPath, arkRulesLoad.errors);
|
|
181
212
|
}
|
|
182
213
|
const loadedContract = loadContract(effectiveConfig, configPath, {
|
|
183
214
|
arkRules: arkRulesLoad.arkRules,
|
|
@@ -212,9 +243,17 @@ export function resolveArchitectureSnapshot({
|
|
|
212
243
|
const rootsPresent = catalogHasEnforcedInvariant(arkRulesLoad.arkRules?.invariants)
|
|
213
244
|
? declaredCoverageRootsPresent(root, effectiveConfig.coverage)
|
|
214
245
|
: true;
|
|
246
|
+
// Version floor evidence (#338): zero extra I/O unless a floored key is in use.
|
|
247
|
+
const pinEvidence = configVersionFloors(effectiveConfig.rules)
|
|
248
|
+
? {
|
|
249
|
+
arkgatePins: collectArkgatePins(root, { observeInput }),
|
|
250
|
+
runningArkgateVersion: runningArkgateVersion(),
|
|
251
|
+
}
|
|
252
|
+
: {};
|
|
215
253
|
const analyzed = analyzeTrustedResolvedProject({
|
|
216
254
|
contract: analysisContract,
|
|
217
255
|
facts,
|
|
256
|
+
...pinEvidence,
|
|
218
257
|
...(adopted ? { adopted: true } : {}),
|
|
219
258
|
...(adopted && pathPresent === false ? { invariantTestsPathPresent: false } : {}),
|
|
220
259
|
...(rootsPresent === false ? { coverageRootsPresent: false } : {}),
|
|
@@ -229,7 +268,8 @@ export function resolveArchitectureSnapshot({
|
|
|
229
268
|
);
|
|
230
269
|
const result = {
|
|
231
270
|
violations: analyzed.ir.violations,
|
|
232
|
-
|
|
271
|
+
// ADR 0012 D2: an unreferenced arkrules/*.json is advisory drift, never a failure.
|
|
272
|
+
warnings: [...analyzed.ir.warnings, ...arkRulesDriftWarnings(arkRulesLoad.warnings)],
|
|
233
273
|
safety: analyzed.safety,
|
|
234
274
|
parseHealth,
|
|
235
275
|
completeness: analyzed.completeness,
|
|
@@ -252,8 +292,14 @@ export function resolveArchitectureSnapshot({
|
|
|
252
292
|
|
|
253
293
|
/**
|
|
254
294
|
* Full architecture scan for governed files.
|
|
255
|
-
*
|
|
295
|
+
*
|
|
296
|
+
* `graphProjection: true` (doctor / report) also returns `importGraph`, a compact
|
|
297
|
+
* importer index projected from the resolved facts before they are released
|
|
298
|
+
* (ADR 0037 D1). The verdict fields are the same object either way.
|
|
299
|
+
* @returns {{ violations: object[], warnings: object[], importGraph?: object }}
|
|
256
300
|
*/
|
|
257
301
|
export function runArchitectureScan(options) {
|
|
258
|
-
|
|
302
|
+
const snapshot = resolveArchitectureSnapshot({ ...options, captureInputs: false });
|
|
303
|
+
if (options?.graphProjection !== true) return snapshot.result;
|
|
304
|
+
return { ...snapshot.result, importGraph: projectImporterIndex(snapshot.facts) };
|
|
259
305
|
}
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* Pure CLI helper (bin/lib/ark-order-doctor.mjs). Zero Node I/O.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { composeMergePlanesHonesty, extraMergeTeethAllowed, isArkOrderRuleId, } from './extra-merge-teeth.mjs';
|
|
11
|
+
import { composeMergePlanesHonesty, extraMergeTeethAllowed, isArkOrderRuleId, mergePlanesArkRulesInput, projectExtraPlaneStatus, } from './extra-merge-teeth.mjs';
|
|
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
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
|
}
|
|
@@ -54,17 +54,7 @@ export function summarizeArkOrderSection(input = {}) {
|
|
|
54
54
|
const residualCount = uniqueIds.length;
|
|
55
55
|
const mergePlanes = composeMergePlanesHonesty({
|
|
56
56
|
classification: input.classification,
|
|
57
|
-
arkRules:
|
|
58
|
-
active: input.arkRules?.active === true,
|
|
59
|
-
structureEnforced: input.arkRules?.structureEnforced,
|
|
60
|
-
structureTotal: input.arkRules?.structureTotal,
|
|
61
|
-
structureAdvisory: input.arkRules?.structureAdvisory,
|
|
62
|
-
invariantEnforced: input.arkRules?.invariantEnforced,
|
|
63
|
-
invariantTotal: input.arkRules?.invariantTotal,
|
|
64
|
-
invariantAdvisory: input.arkRules?.invariantAdvisory,
|
|
65
|
-
covered: input.arkRules?.covered,
|
|
66
|
-
uncovered: input.arkRules?.uncovered,
|
|
67
|
-
},
|
|
57
|
+
arkRules: mergePlanesArkRulesInput(input.arkRules),
|
|
68
58
|
arkRun: {
|
|
69
59
|
present: input.arkRun?.present === true,
|
|
70
60
|
mode: input.arkRun?.mode ?? null,
|
|
@@ -110,22 +100,7 @@ export function summarizeArkOrderSection(input = {}) {
|
|
|
110
100
|
}
|
|
111
101
|
/** Thin status slice — counts only; residual null means unknown, not green. */
|
|
112
102
|
export function projectStatusArkOrder(input = {}) {
|
|
113
|
-
|
|
114
|
-
const mode = closedMode(input.mode);
|
|
115
|
-
const residualRaw = input.residual;
|
|
116
|
-
let residual = null;
|
|
117
|
-
if (typeof residualRaw === 'number' && Number.isFinite(residualRaw) && residualRaw >= 0) {
|
|
118
|
-
residual = Math.floor(residualRaw);
|
|
119
|
-
}
|
|
120
|
-
if (!present)
|
|
121
|
-
residual = residual == null ? 0 : residual;
|
|
122
|
-
return {
|
|
123
|
-
notAScore: true,
|
|
124
|
-
present,
|
|
125
|
-
mode: present ? mode : null,
|
|
126
|
-
extraMergeTeeth: present && mode === 'enforced' && input.extraMergeTeeth === true,
|
|
127
|
-
residual,
|
|
128
|
-
};
|
|
103
|
+
return projectExtraPlaneStatus(input);
|
|
129
104
|
}
|
|
130
105
|
export function formatArkOrderDoctorLines(section) {
|
|
131
106
|
if (!section || section.notAScore !== true)
|
|
@@ -8,14 +8,13 @@
|
|
|
8
8
|
* Pure CLI helper (bin/lib/ark-order-facts.mjs). Zero Node I/O.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return IO_IMPORT_HINT_RE.test(content) || IO_ALIAS_IMPORT_RE.test(content);
|
|
11
|
+
import { XI_TTL_KEY_RE } from './ark-order-types.mjs';
|
|
12
|
+
import { globToRegExp } from '../ark-layer-match.mjs';
|
|
13
|
+
import { sourceHasPersistenceWrite, sourceImportsPersistenceDriverText } from './persistence-write-hint.mjs';
|
|
14
|
+
const ARKORDER_PLANE_FACTORY = 'createOrderPlane';
|
|
15
|
+
/** XIWRITE-001 `appliesTo` globs use the layer-glob engine (`globToRegExp`). */
|
|
16
|
+
export function arkOrderGlobToRegExp(pattern) {
|
|
17
|
+
return globToRegExp(pattern);
|
|
19
18
|
}
|
|
20
19
|
function escapeRegExp(value) {
|
|
21
20
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -36,6 +35,45 @@ function stripCommentsPreservingLines(content) {
|
|
|
36
35
|
.replace(/\/\*[\s\S]*?\*\//g, (block) => block.replace(/[^\n]/g, ' '))
|
|
37
36
|
.replace(/(^|[^:])\/\/.*$/gm, (line) => line.replace(/\/\/.*$/, (c) => ' '.repeat(c.length)));
|
|
38
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Specifiers naming `arkgate/order` in one source text (static import / export-from,
|
|
40
|
+
* `require()`, `import()`). Editor-path twin of the resolved dependency facts the
|
|
41
|
+
* kernel-in-domain sensor reads in CI — syntax only, no layer verdict here.
|
|
42
|
+
*/
|
|
43
|
+
export function extractArkOrderModuleImportsFromSource(file, content) {
|
|
44
|
+
const source = stripCommentsPreservingLines(content);
|
|
45
|
+
const out = [];
|
|
46
|
+
const fromRe = /\b(import|export)(\s+type)?\s+(?:[^;]*?\s+from\s*)?['"]([^'"]+)['"]/g;
|
|
47
|
+
let match;
|
|
48
|
+
while ((match = fromRe.exec(source)) !== null) {
|
|
49
|
+
const specifier = match[3] ?? '';
|
|
50
|
+
if (!isArkOrderModuleSpecifier(specifier))
|
|
51
|
+
continue;
|
|
52
|
+
out.push({
|
|
53
|
+
from: file,
|
|
54
|
+
specifier,
|
|
55
|
+
kind: match[1] === 'export' ? 'export' : 'import',
|
|
56
|
+
typeOnly: Boolean(match[2]),
|
|
57
|
+
line: lineAt(content, match.index),
|
|
58
|
+
resolution: 'resolved-external',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const callRe = /\b(require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
62
|
+
while ((match = callRe.exec(source)) !== null) {
|
|
63
|
+
const specifier = match[2] ?? '';
|
|
64
|
+
if (!isArkOrderModuleSpecifier(specifier))
|
|
65
|
+
continue;
|
|
66
|
+
out.push({
|
|
67
|
+
from: file,
|
|
68
|
+
specifier,
|
|
69
|
+
kind: match[1] === 'import' ? 'dynamic-import' : 'require',
|
|
70
|
+
typeOnly: false,
|
|
71
|
+
line: lineAt(content, match.index),
|
|
72
|
+
resolution: 'resolved-external',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
39
77
|
export function extractArkOrderPlaneCallsFromSource(file, content) {
|
|
40
78
|
const source = stripCommentsPreservingLines(content);
|
|
41
79
|
const facts = [];
|
|
@@ -46,19 +84,190 @@ export function extractArkOrderPlaneCallsFromSource(file, content) {
|
|
|
46
84
|
}
|
|
47
85
|
return facts;
|
|
48
86
|
}
|
|
49
|
-
|
|
87
|
+
const PLANE_BINDING_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=;\n]+)?=\s*createOrderPlane\s*(?:<[^>]*>)?\s*\(/g;
|
|
88
|
+
/**
|
|
89
|
+
* Conventional plane names: `plane`, `orderPlane`, `billingPlane`, … (not the factory).
|
|
90
|
+
* A name alone is never evidence (`clipPlane.set`, `controlPlane.update` are not ArkOrder):
|
|
91
|
+
* it counts only in a file that imports `arkgate/order`, or on a named import from a
|
|
92
|
+
* declared `planeRoots` module.
|
|
93
|
+
*/
|
|
94
|
+
const CONVENTIONAL_PLANE_NAME_RE = /^(?:plane|[A-Za-z_$][\w$]*Plane)$/;
|
|
95
|
+
/** `plane: OrderPlane` — parameters, fields, and variables annotated with the plane type. */
|
|
96
|
+
const ORDER_PLANE_ANNOTATION_RE = /\b([A-Za-z_$][\w$]*)\s*[?!]?\s*:\s*(?:Readonly\s*<\s*)?OrderPlane\b/g;
|
|
97
|
+
const ARKORDER_IMPORT_RE = /\bfrom\s+['"]arkgate\/order(?:\/[^'"]*)?['"]|\brequire\s*\(\s*['"]arkgate\/order(?:\/[^'"]*)?['"]|\bimport\s*\(\s*['"]arkgate\/order(?:\/[^'"]*)?['"]/;
|
|
98
|
+
const NAMED_IMPORT_RE = /\bimport\s+(?:type\s+)?\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g;
|
|
99
|
+
const RELATIVE_EXTENSIONS = ['', '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
|
|
100
|
+
/** Trailing identifier, allowing a non-null `!` and optional-chain `?` before the dot. */
|
|
101
|
+
const TRAILING_IDENTIFIER_RE = /([A-Za-z_$][\w$]*)\s*!?\s*\??\s*$/;
|
|
102
|
+
/** Leading identifier inside a parenthesized receiver: `x as T`, `<T>x`, `x!`, `x`. */
|
|
103
|
+
const WRAPPED_IDENTIFIER_RE = /^\s*(?:<[^<>]*>\s*)?([A-Za-z_$][\w$]*)\s*!?\s*(?:\b(?:as|satisfies)\b[\s\S]*)?$/;
|
|
104
|
+
/** Identifiers bound to `createOrderPlane(...)` in this file. */
|
|
105
|
+
function arkOrderPlaneBindings(content) {
|
|
106
|
+
const source = stripCommentsPreservingLines(content);
|
|
107
|
+
const names = new Set();
|
|
108
|
+
const re = new RegExp(PLANE_BINDING_RE.source, 'g');
|
|
109
|
+
let match;
|
|
110
|
+
while ((match = re.exec(source)) !== null) {
|
|
111
|
+
if (match[1])
|
|
112
|
+
names.add(match[1]);
|
|
113
|
+
}
|
|
114
|
+
return [...names].sort();
|
|
115
|
+
}
|
|
116
|
+
function normalizeRelativePath(parts) {
|
|
117
|
+
const out = [];
|
|
118
|
+
for (const part of parts) {
|
|
119
|
+
if (part === '' || part === '.')
|
|
120
|
+
continue;
|
|
121
|
+
if (part === '..') {
|
|
122
|
+
if (out.length === 0)
|
|
123
|
+
return '';
|
|
124
|
+
out.pop();
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
out.push(part);
|
|
128
|
+
}
|
|
129
|
+
return out.join('/');
|
|
130
|
+
}
|
|
131
|
+
/** Project-relative candidates for a relative specifier (lexical; no tsconfig paths). */
|
|
132
|
+
function relativeModuleCandidates(file, specifier) {
|
|
133
|
+
if (!specifier.startsWith('./') && !specifier.startsWith('../'))
|
|
134
|
+
return [];
|
|
135
|
+
const dir = file.replace(/\\/g, '/').split('/').slice(0, -1);
|
|
136
|
+
const base = normalizeRelativePath([...dir, ...specifier.split('/')]);
|
|
137
|
+
if (!base)
|
|
138
|
+
return [];
|
|
139
|
+
const stem = base.replace(/\.(?:[cm]?js|jsx)$/, '');
|
|
140
|
+
const out = new Set();
|
|
141
|
+
for (const root of [base, stem]) {
|
|
142
|
+
for (const ext of RELATIVE_EXTENSIONS)
|
|
143
|
+
out.add(`${root}${ext}`);
|
|
144
|
+
for (const ext of RELATIVE_EXTENSIONS.slice(1))
|
|
145
|
+
out.add(`${root}/index${ext}`);
|
|
146
|
+
}
|
|
147
|
+
return [...out];
|
|
148
|
+
}
|
|
149
|
+
function specifierResolvesToPlaneRoot(file, specifier, planeRoots) {
|
|
150
|
+
const candidates = relativeModuleCandidates(file, specifier);
|
|
151
|
+
if (candidates.length === 0)
|
|
152
|
+
return false;
|
|
153
|
+
return planeRoots.some((pattern) => {
|
|
154
|
+
let re;
|
|
155
|
+
try {
|
|
156
|
+
re = arkOrderGlobToRegExp(pattern);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
return candidates.some((candidate) => re.test(candidate));
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Identifiers that name an order plane in this file, from direct evidence only:
|
|
166
|
+
* bound to `createOrderPlane(...)`, annotated `: OrderPlane`, or a `plane` / `*Plane`
|
|
167
|
+
* named import from a declared plane root. `isArkOrderFile` reports an `arkgate/order`
|
|
168
|
+
* import, which admits the conventional names file-wide.
|
|
169
|
+
*/
|
|
170
|
+
function arkOrderPlaneReceivers(file, content, options = {}) {
|
|
171
|
+
const source = stripCommentsPreservingLines(content);
|
|
172
|
+
const names = new Set(arkOrderPlaneBindings(content));
|
|
173
|
+
const annotated = new RegExp(ORDER_PLANE_ANNOTATION_RE.source, 'g');
|
|
174
|
+
let match;
|
|
175
|
+
while ((match = annotated.exec(source)) !== null) {
|
|
176
|
+
if (match[1])
|
|
177
|
+
names.add(match[1]);
|
|
178
|
+
}
|
|
179
|
+
const planeRoots = options.planeRoots ?? [];
|
|
180
|
+
if (planeRoots.length > 0) {
|
|
181
|
+
const imports = new RegExp(NAMED_IMPORT_RE.source, 'g');
|
|
182
|
+
while ((match = imports.exec(source)) !== null) {
|
|
183
|
+
if (!specifierResolvesToPlaneRoot(file, match[2] ?? '', planeRoots))
|
|
184
|
+
continue;
|
|
185
|
+
for (const element of (match[1] ?? '').split(',')) {
|
|
186
|
+
const parts = element
|
|
187
|
+
.trim()
|
|
188
|
+
.replace(/^type\s+/, '')
|
|
189
|
+
.split(/\s+as\s+/);
|
|
190
|
+
const imported = parts[0]?.trim() ?? '';
|
|
191
|
+
const local = (parts[1] ?? parts[0] ?? '').trim();
|
|
192
|
+
if (!local || !/^[A-Za-z_$][\w$]*$/.test(local))
|
|
193
|
+
continue;
|
|
194
|
+
if (CONVENTIONAL_PLANE_NAME_RE.test(imported) || CONVENTIONAL_PLANE_NAME_RE.test(local)) {
|
|
195
|
+
names.add(local);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
names.delete(ARKORDER_PLANE_FACTORY);
|
|
201
|
+
return { names: [...names].sort(), isArkOrderFile: ARKORDER_IMPORT_RE.test(source) };
|
|
202
|
+
}
|
|
203
|
+
const KEYWORDS_BEFORE_PAREN = new Set([
|
|
204
|
+
'return', 'await', 'void', 'typeof', 'yield', 'in', 'of', 'case', 'else', 'do', 'throw', 'delete',
|
|
205
|
+
]);
|
|
206
|
+
/** True when `(` at this point opens call arguments (`f(x).update(`), not a wrapper. */
|
|
207
|
+
function isCallBefore(prefix) {
|
|
208
|
+
const tail = prefix.replace(/\s+$/, '');
|
|
209
|
+
if (/[)\]]$/.test(tail))
|
|
210
|
+
return true;
|
|
211
|
+
const word = /([A-Za-z_$][\w$]*)$/.exec(tail);
|
|
212
|
+
return word !== null && !KEYWORDS_BEFORE_PAREN.has(word[1]);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Receiver identifier right before a `.method(` — unwraps `?.`, `!`, and one balanced
|
|
216
|
+
* `( x as T )` / `( x satisfies T )` / `(<T>x)` / `(x)` wrapper (T may itself contain
|
|
217
|
+
* parentheses). `this.plane` yields `plane`.
|
|
218
|
+
*/
|
|
219
|
+
function receiverBefore(before) {
|
|
220
|
+
const trimmed = before.replace(/\s*!?\s*\??\s*$/, '');
|
|
221
|
+
if (!trimmed.endsWith(')')) {
|
|
222
|
+
const match = TRAILING_IDENTIFIER_RE.exec(trimmed);
|
|
223
|
+
return match ? match[1] ?? null : null;
|
|
224
|
+
}
|
|
225
|
+
let depth = 0;
|
|
226
|
+
for (let index = trimmed.length - 1; index >= 0; index -= 1) {
|
|
227
|
+
const ch = trimmed[index];
|
|
228
|
+
if (ch === ')')
|
|
229
|
+
depth += 1;
|
|
230
|
+
else if (ch === '(') {
|
|
231
|
+
depth -= 1;
|
|
232
|
+
if (depth === 0) {
|
|
233
|
+
if (isCallBefore(trimmed.slice(0, index)))
|
|
234
|
+
return null;
|
|
235
|
+
const inner = trimmed.slice(index + 1, trimmed.length - 1);
|
|
236
|
+
const match = WRAPPED_IDENTIFIER_RE.exec(inner);
|
|
237
|
+
return match ? match[1] ?? null : null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
function planeReceiverContext(file, content, options) {
|
|
244
|
+
const resolved = arkOrderPlaneReceivers(file, content, options);
|
|
245
|
+
return { names: new Set(resolved.names), isArkOrderFile: resolved.isArkOrderFile };
|
|
246
|
+
}
|
|
247
|
+
function isPlaneReceiver(name, context) {
|
|
248
|
+
if (!name || name === ARKORDER_PLANE_FACTORY)
|
|
249
|
+
return false;
|
|
250
|
+
if (context.names.has(name))
|
|
251
|
+
return true;
|
|
252
|
+
return context.isArkOrderFile && CONVENTIONAL_PLANE_NAME_RE.test(name);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Direct evidence only: `.update|patch|set|mutate(` whose receiver is a plane (see
|
|
256
|
+
* arkOrderPlaneReceivers). EOSF5-001: Map / URLSearchParams / React `order.set` /
|
|
257
|
+
* `prisma.x.update` are not ξ mutation, even inside the plane-root file; nor is
|
|
258
|
+
* `clipPlane.set` / `controlPlane.update` in a file with no ArkOrder evidence.
|
|
259
|
+
*/
|
|
260
|
+
export function extractArkOrderGenericUpdatesFromSource(file, content, options = {}) {
|
|
50
261
|
const source = stripCommentsPreservingLines(content);
|
|
262
|
+
const context = planeReceiverContext(file, content, options);
|
|
51
263
|
const facts = [];
|
|
52
264
|
const re = /\.((?:update|patch|set|mutate))\s*\(/g;
|
|
53
265
|
let match;
|
|
54
266
|
while ((match = re.exec(source)) !== null) {
|
|
55
267
|
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)) {
|
|
268
|
+
const before = source.slice(Math.max(0, match.index - 160), match.index);
|
|
269
|
+
if (!isPlaneReceiver(receiverBefore(before), context))
|
|
60
270
|
continue;
|
|
61
|
-
}
|
|
62
271
|
facts.push({ file, line: lineAt(content, match.index), method });
|
|
63
272
|
}
|
|
64
273
|
return facts;
|
|
@@ -71,7 +280,7 @@ export function extractArkOrderXiFieldWritesFromSource(file, content, xiKeys) {
|
|
|
71
280
|
if (xiKeys.length === 0)
|
|
72
281
|
return [];
|
|
73
282
|
const source = stripCommentsPreservingLines(content);
|
|
74
|
-
if (!
|
|
283
|
+
if (!sourceImportsPersistenceDriverText(source) || !sourceHasPersistenceWrite(source))
|
|
75
284
|
return [];
|
|
76
285
|
const facts = [];
|
|
77
286
|
const seen = new Set();
|
|
@@ -91,11 +300,24 @@ export function extractArkOrderXiFieldWritesFromSource(file, content, xiKeys) {
|
|
|
91
300
|
}
|
|
92
301
|
return facts;
|
|
93
302
|
}
|
|
94
|
-
/**
|
|
303
|
+
/**
|
|
304
|
+
* Holder names that read as a Release or ξ store: whole-word `xi` / `release` /
|
|
305
|
+
* `pattern` / `house` / `current`; an `xi` camelCase head (`xiNext`, `xiState`); or a
|
|
306
|
+
* `…Xi` / `…Release` / `…Pattern` camelCase tail (`nextXi`, `currentRelease`).
|
|
307
|
+
* Trade-off (ADR 0013, prefer false negatives): names that only *start* with
|
|
308
|
+
* release/current/pattern (`releaseState`, `currentResidual`, `patternResult`,
|
|
309
|
+
* `releaseDate`) are not evidence, so `this.releaseState = plane.ingest(e)` stays silent.
|
|
310
|
+
*/
|
|
311
|
+
const INGEST_XI_HOLDER_SOURCE = '(?:xi(?:[A-Z][\\w$]*)?|release|pattern|house|current|[A-Za-z_$][\\w$]*?(?:Xi|Release|Pattern))(?![\\w$])';
|
|
312
|
+
/**
|
|
313
|
+
* ingest() assigned into a Release / ξ holder (INGEST_XI_HOLDER_SOURCE), as a binding
|
|
314
|
+
* or a property write such as `store.xi =` / `release.xi[k] =` / `this.nextXi =`.
|
|
315
|
+
* Comparisons (`===`) and arrows (`=>`) are not evidence.
|
|
316
|
+
*/
|
|
95
317
|
export function extractArkOrderIngestWritesXiFromSource(file, content) {
|
|
96
318
|
const source = stripCommentsPreservingLines(content);
|
|
97
319
|
const facts = [];
|
|
98
|
-
const re =
|
|
320
|
+
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
321
|
let match;
|
|
100
322
|
while ((match = re.exec(source)) !== null) {
|
|
101
323
|
facts.push({ file, line: lineAt(content, match.index) });
|
|
@@ -117,3 +339,83 @@ export function extractArkOrderReleaseKeyCountsFromSource(file, content) {
|
|
|
117
339
|
}
|
|
118
340
|
return facts;
|
|
119
341
|
}
|
|
342
|
+
/** Keys of a flat object-literal body: `key: v`, `'key': v`, and shorthand `key`. */
|
|
343
|
+
function literalKeys(body) {
|
|
344
|
+
const keys = [];
|
|
345
|
+
for (const entry of body.split(',')) {
|
|
346
|
+
const match = /^\s*(['"]?)([A-Za-z_$][\w$]*)\1\s*(?::|$)/.exec(entry);
|
|
347
|
+
if (match?.[2])
|
|
348
|
+
keys.push(match[2]);
|
|
349
|
+
}
|
|
350
|
+
return keys;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Freshness keys in the ξ literal (first argument) of `plane.release({...})` /
|
|
354
|
+
* `plane.proposeRelease({...})`. σ (the second argument) may carry freshUntil — that
|
|
355
|
+
* is where freshness belongs. The receiver must be a plane (arkOrderPlaneReceivers), so
|
|
356
|
+
* a lock/lease `release({ ttl })` is never evidence. Shorthand `{ plan, ttl }` counts.
|
|
357
|
+
*/
|
|
358
|
+
export function extractArkOrderXiTtlKeysFromSource(file, content, options = {}) {
|
|
359
|
+
const source = stripCommentsPreservingLines(content);
|
|
360
|
+
const context = planeReceiverContext(file, content, options);
|
|
361
|
+
const facts = [];
|
|
362
|
+
const re = /\.(?:release|proposeRelease)\s*\(\s*\{([^}]*)\}/g;
|
|
363
|
+
let match;
|
|
364
|
+
while ((match = re.exec(source)) !== null) {
|
|
365
|
+
const before = source.slice(Math.max(0, match.index - 160), match.index);
|
|
366
|
+
if (!isPlaneReceiver(receiverBefore(before), context))
|
|
367
|
+
continue;
|
|
368
|
+
for (const key of literalKeys(match[1] ?? '')) {
|
|
369
|
+
if (!XI_TTL_KEY_RE.test(key))
|
|
370
|
+
continue;
|
|
371
|
+
facts.push({ file, line: lineAt(content, match.index), key });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return facts;
|
|
375
|
+
}
|
|
376
|
+
function stringLiterals(body) {
|
|
377
|
+
const out = [];
|
|
378
|
+
const re = /(['"`])([^'"`\n\\]+)\1/g;
|
|
379
|
+
let match;
|
|
380
|
+
while ((match = re.exec(body)) !== null) {
|
|
381
|
+
if (match[2])
|
|
382
|
+
out.push(match[2]);
|
|
383
|
+
}
|
|
384
|
+
return out;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Direct evidence only: in a file that calls createOrderPlane, a literal
|
|
388
|
+
* `allowedKinds: [...]` entry that a literal `cannotObserve: [...]` denies. Computed
|
|
389
|
+
* or imported arrays stay silent (inference never blocks) — the runtime plane still
|
|
390
|
+
* throws ARKORDER_INFORMATION_BUDGET before such a Release is persisted.
|
|
391
|
+
*/
|
|
392
|
+
export function extractArkOrderBudgetLeaksFromSource(file, content) {
|
|
393
|
+
const source = stripCommentsPreservingLines(content);
|
|
394
|
+
if (!/\bcreateOrderPlane\b/.test(source))
|
|
395
|
+
return [];
|
|
396
|
+
const denied = new Set();
|
|
397
|
+
const deniedRe = /\bcannotObserve\s*:\s*\[([^\]]*)\]/g;
|
|
398
|
+
let match;
|
|
399
|
+
while ((match = deniedRe.exec(source)) !== null) {
|
|
400
|
+
for (const kind of stringLiterals(match[1] ?? ''))
|
|
401
|
+
denied.add(kind);
|
|
402
|
+
}
|
|
403
|
+
if (denied.size === 0)
|
|
404
|
+
return [];
|
|
405
|
+
const facts = [];
|
|
406
|
+
const seen = new Set();
|
|
407
|
+
const allowedRe = /\ballowedKinds\s*:\s*\[([^\]]*)\]/g;
|
|
408
|
+
while ((match = allowedRe.exec(source)) !== null) {
|
|
409
|
+
const line = lineAt(content, match.index);
|
|
410
|
+
for (const kind of stringLiterals(match[1] ?? '')) {
|
|
411
|
+
if (!denied.has(kind))
|
|
412
|
+
continue;
|
|
413
|
+
const stamp = `${line}:${kind}`;
|
|
414
|
+
if (seen.has(stamp))
|
|
415
|
+
continue;
|
|
416
|
+
seen.add(stamp);
|
|
417
|
+
facts.push({ file, line, kind });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return facts;
|
|
421
|
+
}
|