arkgate 4.8.9 → 4.8.11
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 +111 -3
- package/README.md +5 -4
- package/bin/ark-check-runtime.mjs +8 -2
- package/bin/ark-check.mjs +3 -0
- package/bin/ark.mjs +18 -10
- package/bin/lib/adapter-contract-types.mjs +137 -0
- package/bin/lib/adapter-contract.mjs +4 -180
- package/bin/lib/adapter-finding-refs.mjs +63 -0
- package/bin/lib/agent-projection-formatters.mjs +151 -0
- package/bin/lib/agent-projection-merge.mjs +148 -0
- package/bin/lib/agent-projection-types.mjs +42 -0
- package/bin/lib/agent-projection.mjs +3 -309
- package/bin/lib/analysis-engine.mjs +6 -6
- package/bin/lib/architecture-scan.mjs +91 -4
- package/bin/lib/ark-order-facts.mjs +11 -4
- package/bin/lib/arkrule-file-hints.mjs +255 -20
- package/bin/lib/arkrules-sensors.mjs +364 -68
- package/bin/lib/baseline-key.mjs +45 -1
- package/bin/lib/config-contract.mjs +9 -3
- package/bin/lib/diagnostic-catalog.mjs +1 -0
- package/bin/lib/doctor-human.mjs +3 -3
- package/bin/lib/doctor-next-actions.mjs +3 -1
- package/bin/lib/field-install.mjs +23 -2
- package/bin/lib/first-run-help.mjs +69 -5
- package/bin/lib/project-root.mjs +70 -4
- package/bin/lib/resolved-candidate-facts.mjs +82 -1
- package/bin/lib/rules-inventory.mjs +7 -3
- package/bin/lib/upstream-report.mjs +330 -0
- package/bin/lib/violations.mjs +51 -15
- package/dist/{diagnosticCatalog-BrkOiwCk.d.ts → diagnosticCatalog-DiflIock.d.ts} +30 -41
- package/dist/eslint/index.cjs +4 -7
- package/dist/eslint/index.js +4 -7
- package/dist/index.cjs +28 -31
- package/dist/index.d.ts +52 -13
- package/dist/index.js +28 -31
- 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 +3 -3
- package/docs/agent-guide.md +25 -1
- package/docs/ai-gates.md +8 -0
- package/docs/brownfield-adoption.md +30 -0
- package/docs/configuration.md +14 -1
- package/docs/diagnostics.md +10 -0
- package/docs/package-surface.md +5 -3
- package/docs/use.md +11 -0
- package/package.json +3 -2
- package/schemas/ark.config.schema.json +3 -2
- package/server.json +2 -2
- package/templates/agent-skills/ark-explore/SKILL.md +22 -1
- package/templates/skills/ark-explore.md +22 -1
|
@@ -116,7 +116,9 @@ export function collectDoctorNextActions(ctx) {
|
|
|
116
116
|
actions.push('review dirty baseline freezes — fix the contract before trusting green-via-freeze');
|
|
117
117
|
}
|
|
118
118
|
if (ctx.analysisComplete && ctx.staleBaseline > 0) {
|
|
119
|
-
actions.push(
|
|
119
|
+
actions.push(
|
|
120
|
+
'tighten the baseline (--update-baseline --force --contract-session --author <steward>)'
|
|
121
|
+
);
|
|
120
122
|
}
|
|
121
123
|
if (ctx.staleRunners.length > 0) {
|
|
122
124
|
actions.push(
|
|
@@ -83,6 +83,27 @@ function addDevDependencyPreservingFormat(source, version) {
|
|
|
83
83
|
return `${source.slice(0, contentEnd)}${addition}${eol}${rootClosingIndent}${source.slice(rootClose)}`;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
const ARK_CHECK_BIN_RE = /\b(?:ark-check|arkgate-check)(?:\.mjs|\.js)?\b/;
|
|
87
|
+
const ARK_CHECK_RUNNER_RE =
|
|
88
|
+
/(?:^|[\s"'`;|&])(?:npx|pnpm|yarn|npm|bunx?|node)(?:\s|$)/;
|
|
89
|
+
const GITHUB_RUN_KEY_RE = /^\s*(?:-\s+)?run:\s+/;
|
|
90
|
+
const YAML_CHECK_JOB_ID_RE =
|
|
91
|
+
/^\s*(?:-\s+)?['"]?(?:ark-check|arkgate-check)['"]?\s*:/;
|
|
92
|
+
const YAML_CONCURRENCY_GROUP_RE = /^\s*group:\s+/;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* True when the line invokes ark-check / arkgate-check (npx/pnpm/yarn/npm/node/run).
|
|
96
|
+
* YAML concurrency.group and job-id keys that only contain the name are not invocations.
|
|
97
|
+
*/
|
|
98
|
+
export function isArkCheckInvocationLine(command) {
|
|
99
|
+
if (typeof command !== 'string' || !command.trim()) return false;
|
|
100
|
+
if (/^\s*#/.test(command)) return false;
|
|
101
|
+
if (YAML_CHECK_JOB_ID_RE.test(command)) return false;
|
|
102
|
+
if (YAML_CONCURRENCY_GROUP_RE.test(command)) return false;
|
|
103
|
+
if (!ARK_CHECK_BIN_RE.test(command)) return false;
|
|
104
|
+
return ARK_CHECK_RUNNER_RE.test(command) || GITHUB_RUN_KEY_RE.test(command);
|
|
105
|
+
}
|
|
106
|
+
|
|
86
107
|
/**
|
|
87
108
|
* Ensure a check command string includes `--baseline <file>`.
|
|
88
109
|
* Only touches strings that already invoke ark-check / arkgate-check.
|
|
@@ -97,7 +118,7 @@ export function ensureBaselineFlagInCheckCommand(
|
|
|
97
118
|
if (/^\s*#/.test(command)) {
|
|
98
119
|
return { command, changed: false };
|
|
99
120
|
}
|
|
100
|
-
if (
|
|
121
|
+
if (!isArkCheckInvocationLine(command)) {
|
|
101
122
|
return { command, changed: false };
|
|
102
123
|
}
|
|
103
124
|
if (/(?:^|\s)--baseline(?:\s|=|$)/.test(command)) {
|
|
@@ -180,7 +201,7 @@ export function syncBaselineIntoCheckSurfaces(root, opts = {}) {
|
|
|
180
201
|
let fileChanged = false;
|
|
181
202
|
const nextLines = lines.map((line) => {
|
|
182
203
|
if (/^\s*#/.test(line)) return line;
|
|
183
|
-
if (
|
|
204
|
+
if (!isArkCheckInvocationLine(line)) return line;
|
|
184
205
|
if (/(?:^|\s)--baseline(?:\s|=|$)/.test(line)) return line;
|
|
185
206
|
const { command, changed: c } = ensureBaselineFlagInCheckCommand(line, flagRel);
|
|
186
207
|
if (c) {
|
|
@@ -95,12 +95,74 @@ Non-interactive (no TTY): uses the same defaults as --yes — never calls readli
|
|
|
95
95
|
`;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/** `--sensors` is contract + coverage-evidence only — never a full-check pass. */
|
|
99
|
+
export const SENSORS_PARTIAL_MODE_LINE =
|
|
100
|
+
'Contract + coverage-evidence only: no TypeScript, no analysis. Not a validity verdict.';
|
|
101
|
+
|
|
102
|
+
export const SENSORS_DID_NOT_RUN = Object.freeze(['TypeScript', 'analysis']);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Stamp a successful `--sensors --json` payload so agents cannot read exit 0 as
|
|
106
|
+
* a full-check pass. Failure payloads (`sensors.ok === false`) stay untouched.
|
|
107
|
+
*/
|
|
108
|
+
export function stampSensorsPartialModePayload(payload) {
|
|
109
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return payload;
|
|
110
|
+
const sensors = payload.sensors;
|
|
111
|
+
if (!sensors || typeof sensors !== 'object' || Array.isArray(sensors)) return payload;
|
|
112
|
+
if (sensors.ok === false) return payload;
|
|
113
|
+
return {
|
|
114
|
+
...payload,
|
|
115
|
+
sensors: {
|
|
116
|
+
...sensors,
|
|
117
|
+
notAVerdict: true,
|
|
118
|
+
didNotRun: [...SENSORS_DID_NOT_RUN],
|
|
119
|
+
partialMode: 'contract-only',
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* After `runSensors`, name what this mode skipped. Human success prints the
|
|
126
|
+
* line on stdout; JSON success restamps the captured object. Failures reprint
|
|
127
|
+
* as-is so exit 2 does not look like a map.
|
|
128
|
+
*/
|
|
129
|
+
export async function withSensorsPartialModeHonesty(args, run) {
|
|
130
|
+
if (args?.json) {
|
|
131
|
+
const chunks = [];
|
|
132
|
+
const original = console.log;
|
|
133
|
+
console.log = (...parts) => {
|
|
134
|
+
chunks.push(parts.map(String).join(' '));
|
|
135
|
+
};
|
|
136
|
+
try {
|
|
137
|
+
await run();
|
|
138
|
+
} finally {
|
|
139
|
+
console.log = original;
|
|
140
|
+
}
|
|
141
|
+
const text = chunks.join('\n');
|
|
142
|
+
if ((process.exitCode ?? 0) !== 0) {
|
|
143
|
+
if (text) original(text);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
original(JSON.stringify(stampSensorsPartialModePayload(JSON.parse(text)), null, 2));
|
|
148
|
+
} catch {
|
|
149
|
+
original(text);
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
await run();
|
|
154
|
+
if ((process.exitCode ?? 0) === 0) {
|
|
155
|
+
console.log(SENSORS_PARTIAL_MODE_LINE);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
98
159
|
export function checkUsage() {
|
|
99
160
|
return [
|
|
100
161
|
'arkgate-check (alias ark-check) — the architecture check.',
|
|
101
162
|
'',
|
|
102
163
|
' arkgate-check --doctor where you are: one status light, one next action',
|
|
103
164
|
' arkgate-check --strict-merge CI / merge gate (required GitHub status)',
|
|
165
|
+
' arkgate-check --sensors which sensors can ever be enforced (does not run analysis)',
|
|
104
166
|
'',
|
|
105
167
|
'Every flag and command: arkgate-check --help --all',
|
|
106
168
|
].join('\n');
|
|
@@ -124,7 +186,7 @@ export function checkUsageAll() {
|
|
|
124
186
|
' Exit 0 ran and clean, 1 drift remains, 2 could not run (no usable base ref).',
|
|
125
187
|
' ark-check --sensors [--json] every sensor with its tier and whether it can EVER be enforced, plus every declared rule',
|
|
126
188
|
' with its local id, the sensor it delegates to, its source file, its mode and why it can or cannot be promoted.',
|
|
127
|
-
|
|
189
|
+
` ${SENSORS_PARTIAL_MODE_LINE} Exit 0 on a report, 2 if the contract will not load.`,
|
|
128
190
|
' ark-check --promote [<ruleId>] [--json] [--apply]',
|
|
129
191
|
' what enforcing would cost: the findings each advisory rule already produces, from ONE run rather than one run per attempt.',
|
|
130
192
|
' Plan by default; --promote <ruleId> --apply (or --promote=<ruleId>) writes mode "enforced" into the ArkRules file that declares it.',
|
|
@@ -143,12 +205,14 @@ export function checkUsageAll() {
|
|
|
143
205
|
' ark-check --init [--preset hexagonal|layered|feature-sliced|monorepo|ui-surface|vertical-slice|ddd-bounded-contexts|vite-vercel-spa|clean-architecture|onion-architecture] [--force] [--follow-config-root]',
|
|
144
206
|
' --follow-config-root On writes (init/install-agent-gates/migrate --write/…), adopt walked-up monorepo config root (default: keep explicit --root)',
|
|
145
207
|
' ark-check --install-agent-gates [--tools claude,cursor,codex,grok,antigravity] [--require-write-hook <host>] [--skills-only] [--codex-home] [--claude-home] [--grok-home] [--antigravity-home] [--agent-homes] [--force]',
|
|
146
|
-
' ark-check --update-baseline [file]
|
|
208
|
+
' ark-check --update-baseline [file] --force --contract-session --author <steward>',
|
|
209
|
+
' freeze current violations (default .ark-baseline.json). --contract-session is required;',
|
|
210
|
+
' --force when freeze-refuse fires; --author when stewards[] is set.',
|
|
147
211
|
' ark-check --print-config eleven-layer',
|
|
148
212
|
'',
|
|
149
|
-
'Adopting Ark in an existing codebase? Run --update-baseline
|
|
150
|
-
'violations, commit the baseline file, and gate CI with --baseline: only NEW
|
|
151
|
-
'fail the check, so the ratchet only moves toward zero.',
|
|
213
|
+
'Adopting Ark in an existing codebase? Run --update-baseline --force --contract-session --author <steward>',
|
|
214
|
+
'once to freeze existing violations, commit the baseline file, and gate CI with --baseline: only NEW',
|
|
215
|
+
'violations fail the check, so the ratchet only moves toward zero.',
|
|
152
216
|
'',
|
|
153
217
|
'Team parliament: law files (ark.config / arkrules / .ark-baseline.json) cannot ship in',
|
|
154
218
|
'the same diff as product source. --changed --base <ref> checks touched files only.',
|
package/bin/lib/project-root.mjs
CHANGED
|
@@ -88,10 +88,64 @@ export function resolveConfigPathWithinRoot(projectRoot, configPathOrName) {
|
|
|
88
88
|
return { ok: true, configPath };
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
function isFile(absPath) {
|
|
92
|
+
try {
|
|
93
|
+
return Boolean(fs.statSync(absPath, { throwIfNoEntry: false })?.isFile());
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** True when --config is a path (nested or absolute), not a basename to walk. */
|
|
100
|
+
export function configNameIsPath(configName) {
|
|
101
|
+
return (
|
|
102
|
+
typeof configName === 'string' &&
|
|
103
|
+
(path.isAbsolute(configName) || /[\\/]/.test(configName))
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isInsideRoot(root, target) {
|
|
108
|
+
const rel = path.relative(root, target);
|
|
109
|
+
return rel === '' || (!rel.startsWith(`..${path.sep}`) && rel !== '..' && !path.isAbsolute(rel));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolve a nested/absolute --config path without walking parents.
|
|
114
|
+
* `--root examples/app --config examples/app/ark.config.json` must load the nested
|
|
115
|
+
* contract, not latch onto a parent basename walk.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} startDir
|
|
118
|
+
* @param {string} configName
|
|
119
|
+
* @returns {string | null} absolute config file path
|
|
120
|
+
*/
|
|
121
|
+
export function resolveConfigPathCandidate(startDir, configName) {
|
|
122
|
+
if (typeof configName !== 'string' || configName.trim() === '') return null;
|
|
123
|
+
const start = path.resolve(startDir || process.cwd());
|
|
124
|
+
if (path.isAbsolute(configName)) return isFile(configName) ? path.resolve(configName) : null;
|
|
125
|
+
|
|
126
|
+
const fromStart = path.resolve(start, configName);
|
|
127
|
+
if (isFile(fromStart)) return fromStart;
|
|
128
|
+
|
|
129
|
+
const base = path.basename(configName);
|
|
130
|
+
const relDir = path.dirname(configName);
|
|
131
|
+
if (relDir && relDir !== '.') {
|
|
132
|
+
const namedLeaf = path.basename(relDir);
|
|
133
|
+
if (namedLeaf && namedLeaf === path.basename(start)) {
|
|
134
|
+
const stripped = path.join(start, base);
|
|
135
|
+
if (isFile(stripped)) return stripped;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const fromCwd = path.resolve(process.cwd(), configName);
|
|
140
|
+
if (fromCwd !== fromStart && isFile(fromCwd)) return fromCwd;
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
91
144
|
/**
|
|
92
145
|
* Walk parents from startDir looking for configName (default ark.config.json).
|
|
93
146
|
* Bounds: filesystem root, max depth, git root, workspaces package root.
|
|
94
147
|
* Config found at a bound root is accepted; walking above a bound is refused.
|
|
148
|
+
* Nested relative --config (`examples/app/ark.config.json`) is a file path, never a walk-up name.
|
|
95
149
|
*
|
|
96
150
|
* @param {string} startDir
|
|
97
151
|
* @param {string} [configName='ark.config.json']
|
|
@@ -104,17 +158,29 @@ export function findNearestArkConfig(startDir, configName = 'ark.config.json', o
|
|
|
104
158
|
const boundAtWorkspaces = opts.boundAtWorkspacesRoot !== false;
|
|
105
159
|
|
|
106
160
|
if (typeof configName === 'string' && path.isAbsolute(configName)) {
|
|
107
|
-
if (
|
|
161
|
+
if (isFile(configName)) {
|
|
108
162
|
const root = path.dirname(configName);
|
|
109
163
|
const start = path.resolve(startDir || process.cwd());
|
|
110
|
-
return { root, configPath: configName, walkedUp: path.resolve(root) !== start };
|
|
164
|
+
return { root, configPath: path.resolve(configName), walkedUp: path.resolve(root) !== start };
|
|
111
165
|
}
|
|
112
166
|
return null;
|
|
113
167
|
}
|
|
114
168
|
|
|
169
|
+
const name = configName || 'ark.config.json';
|
|
170
|
+
if (configNameIsPath(name)) {
|
|
171
|
+
const resolved = resolveConfigPathCandidate(startDir, name);
|
|
172
|
+
if (!resolved) return null;
|
|
173
|
+
const start = path.resolve(startDir || process.cwd());
|
|
174
|
+
const inside = isInsideRoot(start, resolved);
|
|
175
|
+
return {
|
|
176
|
+
root: inside ? start : path.dirname(resolved),
|
|
177
|
+
configPath: resolved,
|
|
178
|
+
walkedUp: !inside,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
115
182
|
let dir = path.resolve(startDir || process.cwd());
|
|
116
183
|
const start = dir;
|
|
117
|
-
const name = configName || 'ark.config.json';
|
|
118
184
|
let depth = 0;
|
|
119
185
|
for (;;) {
|
|
120
186
|
const candidate = path.join(dir, name);
|
|
@@ -231,7 +297,7 @@ export function resolveEffectiveProjectRoot(startRoot, opts = {}) {
|
|
|
231
297
|
config: configName,
|
|
232
298
|
configPath: found.configPath,
|
|
233
299
|
configRoot: found.root,
|
|
234
|
-
walkedUp:
|
|
300
|
+
walkedUp: found.walkedUp,
|
|
235
301
|
configFound: true,
|
|
236
302
|
writeRootFollowedConfig: adoptWalkedRoot && found.walkedUp,
|
|
237
303
|
};
|
|
@@ -750,6 +750,83 @@ function declaredIntent(value, config) {
|
|
|
750
750
|
);
|
|
751
751
|
}
|
|
752
752
|
|
|
753
|
+
/** Call names whose string arguments are declared intent-reference sites. */
|
|
754
|
+
const INTENT_CALL_NAMES = new Set(['publish', 'subscribe', 'defineIntent', 'registerHandler']);
|
|
755
|
+
|
|
756
|
+
function isSyntaxWrapper(ts, node) {
|
|
757
|
+
return Boolean(
|
|
758
|
+
node &&
|
|
759
|
+
(ts.isParenthesizedExpression(node) ||
|
|
760
|
+
ts.isAsExpression(node) ||
|
|
761
|
+
(typeof ts.isTypeAssertionExpression === 'function' &&
|
|
762
|
+
ts.isTypeAssertionExpression(node)) ||
|
|
763
|
+
(typeof ts.isSatisfiesExpression === 'function' && ts.isSatisfiesExpression(node)))
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function unwrapWrappers(ts, node) {
|
|
768
|
+
let current = node;
|
|
769
|
+
while (current?.parent && isSyntaxWrapper(ts, current.parent)) {
|
|
770
|
+
current = current.parent;
|
|
771
|
+
}
|
|
772
|
+
return current;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function callCalleeName(ts, node) {
|
|
776
|
+
if (!node || !ts.isCallExpression(node)) return undefined;
|
|
777
|
+
const expression = node.expression;
|
|
778
|
+
if (ts.isIdentifier(expression)) return expression.text;
|
|
779
|
+
if (ts.isPropertyAccessExpression(expression)) return expression.name.text;
|
|
780
|
+
return undefined;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function isPublishMetadataSource(ts, sourceProp) {
|
|
784
|
+
const object = sourceProp.parent;
|
|
785
|
+
if (!object || !ts.isObjectLiteralExpression(object)) return false;
|
|
786
|
+
const objectSite = unwrapWrappers(ts, object);
|
|
787
|
+
const objectParent = objectSite.parent;
|
|
788
|
+
if (!objectParent) return false;
|
|
789
|
+
if (ts.isCallExpression(objectParent) && callCalleeName(ts, objectParent) === 'publish') {
|
|
790
|
+
const args = objectParent.arguments;
|
|
791
|
+
return args[1] === objectSite || args[2] === objectSite;
|
|
792
|
+
}
|
|
793
|
+
if (
|
|
794
|
+
ts.isPropertyAssignment(objectParent) &&
|
|
795
|
+
syntaxPropertyName(ts, objectParent.name) === 'metadata'
|
|
796
|
+
) {
|
|
797
|
+
const eventObject = objectParent.parent;
|
|
798
|
+
if (!eventObject) return false;
|
|
799
|
+
const eventSite = unwrapWrappers(ts, eventObject);
|
|
800
|
+
const call = eventSite.parent;
|
|
801
|
+
return Boolean(
|
|
802
|
+
call && ts.isCallExpression(call) && callCalleeName(ts, call) === 'publish'
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** Events, sagas, and publish metadata — not every string that matches a prefix. */
|
|
809
|
+
function isDeclaredIntentSite(ts, node) {
|
|
810
|
+
const siteNode = unwrapWrappers(ts, node);
|
|
811
|
+
const parent = siteNode.parent;
|
|
812
|
+
if (!parent) return false;
|
|
813
|
+
if (ts.isCallExpression(parent)) {
|
|
814
|
+
const name = callCalleeName(ts, parent);
|
|
815
|
+
if (INTENT_CALL_NAMES.has(name) && parent.arguments.some((arg) => arg === siteNode)) {
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (ts.isArrayLiteralExpression(parent)) {
|
|
820
|
+
return isDeclaredIntentSite(ts, parent);
|
|
821
|
+
}
|
|
822
|
+
if (ts.isPropertyAssignment(parent)) {
|
|
823
|
+
const name = syntaxPropertyName(ts, parent.name);
|
|
824
|
+
if (name === 'intent' || name === 'onEvent' || name === 'reactsTo') return true;
|
|
825
|
+
if (name === 'source' && isPublishMetadataSource(ts, parent)) return true;
|
|
826
|
+
}
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
|
|
753
830
|
function mayContainForbiddenCapability(ts, sourceFile, forbiddenGlobals) {
|
|
754
831
|
// Every symbol-aware match originates in an identifier or static string path segment.
|
|
755
832
|
// Inspect decoded AST text so escaped identifiers still take the full checker path.
|
|
@@ -788,7 +865,11 @@ function collectPolicyFacts(ts, sourceFile, relativePath, config) {
|
|
|
788
865
|
: {}),
|
|
789
866
|
});
|
|
790
867
|
}
|
|
791
|
-
if (
|
|
868
|
+
if (
|
|
869
|
+
ts.isStringLiteralLike(node) &&
|
|
870
|
+
declaredIntent(node.text, config) &&
|
|
871
|
+
isDeclaredIntentSite(ts, node)
|
|
872
|
+
) {
|
|
792
873
|
intentReferences.push({
|
|
793
874
|
file: relativePath,
|
|
794
875
|
line: lineOf(sourceFile, node.getStart(sourceFile)),
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Pure CLI helper (bin/lib/rules-inventory.mjs). Zero Node I/O.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { DOMAIN_EVENTS_PUSH_RE, DOMAIN_INVARIANT_WORD_RE, expectedDomainInvariantWordsPhrase, isIdiomaticEventsReset, } from './arkrules-sensors.mjs';
|
|
11
12
|
function lineOf(content, index) {
|
|
12
13
|
return content.slice(0, index).split('\n').length;
|
|
13
14
|
}
|
|
@@ -226,7 +227,7 @@ export function buildRulesInventory(input) {
|
|
|
226
227
|
/(?:^|\/)[^/]*(?:-access)?\.error\./i.test(posix) ||
|
|
227
228
|
/(?:^|\/)errors?(?:\/|$)/i.test(posix);
|
|
228
229
|
if (!isErrorBag) {
|
|
229
|
-
const mutRe = /
|
|
230
|
+
const mutRe = /\bthis\.[A-Za-z_][A-Za-z0-9_]*\s*=(?!=)/g;
|
|
230
231
|
let mut;
|
|
231
232
|
while ((mut = mutRe.exec(content)) !== null) {
|
|
232
233
|
const classStart = content.lastIndexOf('class ', mut.index);
|
|
@@ -239,15 +240,18 @@ export function buildRulesInventory(input) {
|
|
|
239
240
|
if (/\bextends\s+(?:Error|[A-Za-z_$][A-Za-z0-9_$]*Error)\b/.test(classHeader)) {
|
|
240
241
|
continue;
|
|
241
242
|
}
|
|
243
|
+
if (isIdiomaticEventsReset(content, mut.index))
|
|
244
|
+
continue;
|
|
242
245
|
const window = content.slice(Math.max(0, mut.index - 200), mut.index + 200);
|
|
243
|
-
if (
|
|
246
|
+
if (!DOMAIN_INVARIANT_WORD_RE.test(window) &&
|
|
247
|
+
!DOMAIN_EVENTS_PUSH_RE.test(window)) {
|
|
244
248
|
seq += 1;
|
|
245
249
|
candidates.push({
|
|
246
250
|
id: `inv-mut-${seq}`,
|
|
247
251
|
kind: 'mutation-without-guard',
|
|
248
252
|
file,
|
|
249
253
|
line: lineOf(content, mut.index),
|
|
250
|
-
message:
|
|
254
|
+
message: `Domain field mutation without nearby ${expectedDomainInvariantWordsPhrase()}.`,
|
|
251
255
|
confidence: 'heuristic',
|
|
252
256
|
governedLayer,
|
|
253
257
|
suggestedArkRule: {
|