arkgate 2.9.1 → 2.10.0
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 +72 -0
- package/README.md +14 -2
- package/bin/ark-mcp.mjs +282 -102
- package/bin/lib/agent-gates.mjs +181 -6
- package/bin/lib/architecture-scan.mjs +19 -0
- package/bin/lib/auto-patch.mjs +264 -0
- package/bin/lib/doctor-plan.mjs +54 -0
- package/bin/lib/port-proof.mjs +309 -0
- package/bin/lib/prepare-write.mjs +130 -0
- package/bin/lib/remediation.mjs +21 -0
- package/dist/index.cjs +13 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +13 -4
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +1 -1
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.js +1 -1
- package/dist/nestjs/index.js.map +1 -1
- package/dist/runtime/index.cjs +13 -4
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.js +13 -4
- package/dist/runtime/index.js.map +1 -1
- package/docs/agent-guide.md +14 -0
- package/docs/ai-gates.md +43 -3
- package/docs/enthusiast/how-to-agent-gates.md +8 -0
- package/docs/enthusiast/reference-commands.md +1 -1
- package/package.json +2 -1
- package/server.json +2 -2
- package/templates/skills/ark-adopt.md +57 -10
- package/templates/skills/ark-architect.md +33 -2
- package/templates/skills/ark-autopilot.md +75 -20
- package/templates/skills/ark-contract.md +36 -0
- package/templates/skills/ark-coverage.md +81 -27
- package/templates/skills/ark-explain.md +35 -2
- package/templates/skills/ark-explore.md +119 -0
- package/templates/skills/ark-fix.md +47 -2
- package/templates/skills/ark-loop.md +50 -3
- package/templates/skills/ark-place.md +36 -0
- package/templates/skills/ark-runtime.md +36 -0
- package/templates/skills/ark-think.md +63 -12
- package/templates/skills/ark-upgrade.md +33 -1
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W6 — Verified structural transform: port-proof inject binding (single narrow kind).
|
|
3
|
+
*
|
|
4
|
+
* Scope (intentionally tiny — false mechanical-safe is worse than extra judgment):
|
|
5
|
+
* - Exactly one static named value import (no default / namespace / side-effect / export-from)
|
|
6
|
+
* - Binding used ONLY as property-access call receiver: `binding.method(...)`
|
|
7
|
+
* - All uses inside `function` declarations (not module-level, not classes, not arrows)
|
|
8
|
+
* - No require / dynamic import
|
|
9
|
+
*
|
|
10
|
+
* Transform (single file):
|
|
11
|
+
* 1. Remove the import
|
|
12
|
+
* 2. Emit `export type <Port> = { method: (...args: unknown[]) => unknown; ... }`
|
|
13
|
+
* 3. Add `binding: Port` as last parameter of each function that uses it
|
|
14
|
+
* 4. Leave call expressions verbatim (`binding.method(...)`)
|
|
15
|
+
*
|
|
16
|
+
* Static proof of behavior preservation (module-local):
|
|
17
|
+
* If the injected parameter equals the value previously bound by the import, every
|
|
18
|
+
* statement in each rewritten function evaluates identically — call expressions are
|
|
19
|
+
* preserved character-for-character after rebinding. No adapter file is invented;
|
|
20
|
+
* the outer layer must pass the implementation (classic port inject).
|
|
21
|
+
*
|
|
22
|
+
* Fail closed: any unmatched use pattern → not eligible (judgment).
|
|
23
|
+
*/
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {object} ts typescript module
|
|
28
|
+
* @param {string} source
|
|
29
|
+
* @param {{ filePath?: string, importLocalName?: string, importSpecifier?: string }} [opts]
|
|
30
|
+
* @returns {{ eligible: boolean, reason?: string, bindingName?: string, methods?: string[], functionNames?: string[], specifier?: string }}
|
|
31
|
+
*/
|
|
32
|
+
export function provePortProofInject(ts, source, opts = {}) {
|
|
33
|
+
if (!ts || typeof source !== 'string') {
|
|
34
|
+
return { eligible: false, reason: 'missing-ts-or-source' };
|
|
35
|
+
}
|
|
36
|
+
const sf = ts.createSourceFile(
|
|
37
|
+
opts.filePath || 'file.ts',
|
|
38
|
+
source,
|
|
39
|
+
ts.ScriptTarget.Latest,
|
|
40
|
+
true,
|
|
41
|
+
ts.ScriptKind.TS
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
/** @type {import('typescript').ImportDeclaration | null} */
|
|
45
|
+
let targetImport = null;
|
|
46
|
+
let bindingName = null;
|
|
47
|
+
let specifier = null;
|
|
48
|
+
let namedImportCount = 0;
|
|
49
|
+
|
|
50
|
+
for (const stmt of sf.statements) {
|
|
51
|
+
if (!ts.isImportDeclaration(stmt)) continue;
|
|
52
|
+
if (stmt.importClause?.isTypeOnly) continue;
|
|
53
|
+
const clause = stmt.importClause;
|
|
54
|
+
if (!clause) continue; // side-effect
|
|
55
|
+
if (clause.name) {
|
|
56
|
+
// default import — never port-proof
|
|
57
|
+
return { eligible: false, reason: 'default-import' };
|
|
58
|
+
}
|
|
59
|
+
const named = clause.namedBindings;
|
|
60
|
+
if (!named || !ts.isNamedImports(named)) {
|
|
61
|
+
return { eligible: false, reason: 'namespace-or-missing-named' };
|
|
62
|
+
}
|
|
63
|
+
if (named.elements.some((el) => el.isTypeOnly)) {
|
|
64
|
+
// partial type-only mixed — not this transform
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (named.elements.length !== 1) {
|
|
68
|
+
return { eligible: false, reason: 'multi-named-import' };
|
|
69
|
+
}
|
|
70
|
+
namedImportCount += 1;
|
|
71
|
+
if (namedImportCount > 1) {
|
|
72
|
+
return { eligible: false, reason: 'multiple-value-imports' };
|
|
73
|
+
}
|
|
74
|
+
const el = named.elements[0];
|
|
75
|
+
bindingName = el.name.text;
|
|
76
|
+
specifier = stmt.moduleSpecifier && ts.isStringLiteralLike(stmt.moduleSpecifier)
|
|
77
|
+
? stmt.moduleSpecifier.text
|
|
78
|
+
: null;
|
|
79
|
+
if (!specifier || (!specifier.startsWith('./') && !specifier.startsWith('../'))) {
|
|
80
|
+
return { eligible: false, reason: 'non-relative-specifier' };
|
|
81
|
+
}
|
|
82
|
+
if (opts.importLocalName && opts.importLocalName !== bindingName) {
|
|
83
|
+
return { eligible: false, reason: 'binding-mismatch' };
|
|
84
|
+
}
|
|
85
|
+
if (opts.importSpecifier && opts.importSpecifier !== specifier) {
|
|
86
|
+
// soft: still allow if only one import
|
|
87
|
+
}
|
|
88
|
+
targetImport = stmt;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!targetImport || !bindingName) {
|
|
92
|
+
return { eligible: false, reason: 'no-single-named-value-import' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const methods = new Set();
|
|
96
|
+
const functionNames = new Set();
|
|
97
|
+
let freeBindingUse = false;
|
|
98
|
+
|
|
99
|
+
// Walk top-level: only function declarations may use the binding (as method calls).
|
|
100
|
+
function walkTop(node) {
|
|
101
|
+
if (ts.isFunctionDeclaration(node)) {
|
|
102
|
+
if (node.body) {
|
|
103
|
+
visitBody(node.body, node.name?.text);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// Any binding use outside function decls is invalid
|
|
108
|
+
if (ts.isIdentifier(node) && node.text === bindingName) {
|
|
109
|
+
if (node.parent && ts.isImportSpecifier(node.parent)) return;
|
|
110
|
+
freeBindingUse = true;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
ts.forEachChild(node, walkTop);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function visitBody(body, fnName) {
|
|
117
|
+
function walk(node) {
|
|
118
|
+
if (ts.isIdentifier(node) && node.text === bindingName) {
|
|
119
|
+
const parent = node.parent;
|
|
120
|
+
if (
|
|
121
|
+
parent &&
|
|
122
|
+
ts.isPropertyAccessExpression(parent) &&
|
|
123
|
+
parent.expression === node &&
|
|
124
|
+
parent.parent &&
|
|
125
|
+
ts.isCallExpression(parent.parent) &&
|
|
126
|
+
parent.parent.expression === parent
|
|
127
|
+
) {
|
|
128
|
+
methods.add(parent.name.text);
|
|
129
|
+
if (fnName) functionNames.add(fnName);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
freeBindingUse = true;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Nested functions: still require property-call form
|
|
136
|
+
if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node)) {
|
|
137
|
+
// Nested non-declarations: fail closed (narrow transform)
|
|
138
|
+
if (!ts.isFunctionDeclaration(node)) {
|
|
139
|
+
// Check if binding used inside — if yes, ineligible
|
|
140
|
+
let nestedUse = false;
|
|
141
|
+
const check = (n) => {
|
|
142
|
+
if (ts.isIdentifier(n) && n.text === bindingName) nestedUse = true;
|
|
143
|
+
else ts.forEachChild(n, check);
|
|
144
|
+
};
|
|
145
|
+
if (node.body) check(node.body);
|
|
146
|
+
if (nestedUse) freeBindingUse = true;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
ts.forEachChild(node, walk);
|
|
151
|
+
}
|
|
152
|
+
walk(body);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const stmt of sf.statements) {
|
|
156
|
+
walkTop(stmt);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (freeBindingUse) {
|
|
160
|
+
return { eligible: false, reason: 'free-or-non-call-use' };
|
|
161
|
+
}
|
|
162
|
+
if (methods.size === 0 || functionNames.size === 0) {
|
|
163
|
+
return { eligible: false, reason: 'no-method-calls-in-functions' };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
eligible: true,
|
|
168
|
+
bindingName,
|
|
169
|
+
methods: [...methods].sort(),
|
|
170
|
+
functionNames: [...functionNames].sort(),
|
|
171
|
+
specifier,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Apply port-proof inject when prove succeeds.
|
|
177
|
+
* @returns {{ source: string, remediationKind: string, confidence: number, proof: object } | null}
|
|
178
|
+
*/
|
|
179
|
+
export function applyPortProofInject(ts, source, opts = {}) {
|
|
180
|
+
const proof = provePortProofInject(ts, source, opts);
|
|
181
|
+
if (!proof.eligible) return null;
|
|
182
|
+
|
|
183
|
+
const sf = ts.createSourceFile(
|
|
184
|
+
opts.filePath || 'file.ts',
|
|
185
|
+
source,
|
|
186
|
+
ts.ScriptTarget.Latest,
|
|
187
|
+
true,
|
|
188
|
+
ts.ScriptKind.TS
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const bindingName = proof.bindingName;
|
|
192
|
+
const methods = proof.methods;
|
|
193
|
+
const portTypeName = `${capitalize(bindingName)}Port`;
|
|
194
|
+
|
|
195
|
+
// Find import to remove
|
|
196
|
+
/** @type {{ start: number, end: number } | null} */
|
|
197
|
+
let importSpan = null;
|
|
198
|
+
for (const stmt of sf.statements) {
|
|
199
|
+
if (!ts.isImportDeclaration(stmt)) continue;
|
|
200
|
+
const clause = stmt.importClause;
|
|
201
|
+
if (!clause?.namedBindings || !ts.isNamedImports(clause.namedBindings)) continue;
|
|
202
|
+
if (clause.namedBindings.elements.length !== 1) continue;
|
|
203
|
+
if (clause.namedBindings.elements[0].name.text !== bindingName) continue;
|
|
204
|
+
// getStart skips leading trivia so we don't leave a stray indent before the port type.
|
|
205
|
+
importSpan = { start: stmt.getStart(sf), end: stmt.getEnd() };
|
|
206
|
+
if (source[importSpan.end] === '\r') importSpan.end += 1;
|
|
207
|
+
if (source[importSpan.end] === '\n') importSpan.end += 1;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
if (!importSpan) return null;
|
|
211
|
+
|
|
212
|
+
// Functions that need the port param
|
|
213
|
+
const fnEdits = [];
|
|
214
|
+
for (const stmt of sf.statements) {
|
|
215
|
+
if (!ts.isFunctionDeclaration(stmt) || !stmt.name || !stmt.body) continue;
|
|
216
|
+
if (!proof.functionNames.includes(stmt.name.text)) continue;
|
|
217
|
+
// Already has a param named bindingName?
|
|
218
|
+
const params = stmt.parameters || [];
|
|
219
|
+
if (params.some((p) => ts.isIdentifier(p.name) && p.name.text === bindingName)) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
// Fail closed: rest params / non-identifier patterns would yield illegal TS
|
|
223
|
+
// (e.g. function f(...args, db: Port)).
|
|
224
|
+
if (
|
|
225
|
+
params.some(
|
|
226
|
+
(p) =>
|
|
227
|
+
p.dotDotDotToken ||
|
|
228
|
+
!ts.isIdentifier(p.name)
|
|
229
|
+
)
|
|
230
|
+
) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
if (params.length > 0) {
|
|
234
|
+
const at = params[params.length - 1].getEnd();
|
|
235
|
+
fnEdits.push({
|
|
236
|
+
start: at,
|
|
237
|
+
end: at,
|
|
238
|
+
text: `, ${bindingName}: ${portTypeName}`,
|
|
239
|
+
});
|
|
240
|
+
} else {
|
|
241
|
+
const open = source.indexOf('(', stmt.name.getEnd());
|
|
242
|
+
if (open < 0) continue;
|
|
243
|
+
const at = open + 1;
|
|
244
|
+
fnEdits.push({
|
|
245
|
+
start: at,
|
|
246
|
+
end: at,
|
|
247
|
+
text: `${bindingName}: ${portTypeName}`,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (fnEdits.length === 0) return null;
|
|
252
|
+
|
|
253
|
+
const portDecl =
|
|
254
|
+
`export type ${portTypeName} = {\n` +
|
|
255
|
+
methods.map((m) => ` ${m}: (...args: unknown[]) => unknown;`).join('\n') +
|
|
256
|
+
`\n};\n\n`;
|
|
257
|
+
|
|
258
|
+
// Apply edits from end to start
|
|
259
|
+
const edits = [
|
|
260
|
+
{ start: importSpan.start, end: importSpan.end, text: portDecl },
|
|
261
|
+
...fnEdits,
|
|
262
|
+
].sort((a, b) => b.start - a.start);
|
|
263
|
+
|
|
264
|
+
let out = source;
|
|
265
|
+
for (const e of edits) {
|
|
266
|
+
out = out.slice(0, e.start) + e.text + out.slice(e.end);
|
|
267
|
+
}
|
|
268
|
+
if (out === source) return null;
|
|
269
|
+
|
|
270
|
+
// Re-prove on result: binding should no longer need the import; methods still present
|
|
271
|
+
// (as params). Eligibility after transform is "no value import" — proof may fail for
|
|
272
|
+
// different reasons. Validate shape: no import of binding's original specifier as value.
|
|
273
|
+
if (new RegExp(`import\\s*\\{[^}]*\\b${escapeRe(bindingName)}\\b`).test(out)) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
if (!out.includes(`export type ${portTypeName}`)) return null;
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
source: out,
|
|
280
|
+
remediationKind: 'port-proof-inject-binding',
|
|
281
|
+
confidence: 0.8,
|
|
282
|
+
proof: {
|
|
283
|
+
bindingName,
|
|
284
|
+
methods,
|
|
285
|
+
functionNames: proof.functionNames,
|
|
286
|
+
specifier: proof.specifier,
|
|
287
|
+
portTypeName,
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function capitalize(s) {
|
|
293
|
+
if (!s) return 'Port';
|
|
294
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function escapeRe(s) {
|
|
298
|
+
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* True when relative path basename matches a common path segment of the import target.
|
|
303
|
+
* Used only as a soft filter when wiring from ark-check violations.
|
|
304
|
+
*/
|
|
305
|
+
export function specifierLooksLikeTarget(specifier, violationTargetRel) {
|
|
306
|
+
if (!specifier || !violationTargetRel) return true;
|
|
307
|
+
const base = path.basename(violationTargetRel, path.extname(violationTargetRel));
|
|
308
|
+
return specifier.includes(base) || violationTargetRel.includes(specifier.replace(/^\.\//, ''));
|
|
309
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W2 — ark_prepare_write composition helpers.
|
|
3
|
+
*
|
|
4
|
+
* Place + constrain + validate + autoPatch + judgmentBrief + content identity.
|
|
5
|
+
* Pure-ish: no second architecture contract — callers pass placement + validate.
|
|
6
|
+
*/
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import { validateWithAutoPatch } from './auto-patch.mjs';
|
|
9
|
+
import { classifyRemediation, enrichViolationWithFixClass } from './remediation.mjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Stable content identity for host commit / cache keys.
|
|
13
|
+
* @param {string} source
|
|
14
|
+
* @returns {{ contentHash: string, byteLength: number }}
|
|
15
|
+
*/
|
|
16
|
+
export function contentIdentity(source) {
|
|
17
|
+
const text = typeof source === 'string' ? source : '';
|
|
18
|
+
const contentHash = `sha256:${crypto.createHash('sha256').update(text, 'utf8').digest('hex')}`;
|
|
19
|
+
return { contentHash, byteLength: Buffer.byteLength(text, 'utf8') };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One judgment decision for the agent when autoPatch is absent or insufficient.
|
|
24
|
+
* @param {Array<object>} violations
|
|
25
|
+
* @returns {null | { fixClass: string, decision: string, remediationClass: string, remediationKind?: string }}
|
|
26
|
+
*/
|
|
27
|
+
export function buildJudgmentBrief(violations) {
|
|
28
|
+
if (!Array.isArray(violations) || violations.length === 0) return null;
|
|
29
|
+
for (const v of violations) {
|
|
30
|
+
const ruleId = v.ruleId || v.code;
|
|
31
|
+
const shaped = {
|
|
32
|
+
ruleId,
|
|
33
|
+
typeOnly: v.typeOnly ?? v.details?.typeOnly,
|
|
34
|
+
sourcePureTypeModule: v.sourcePureTypeModule,
|
|
35
|
+
targetTypeOnlyExports: v.targetTypeOnlyExports,
|
|
36
|
+
namedBindingsTypeOnly: v.namedBindingsTypeOnly,
|
|
37
|
+
peerIsolation: v.peerIsolation ?? v.details?.peerIsolation,
|
|
38
|
+
edgeKind: v.edgeKind ?? v.details?.importKind,
|
|
39
|
+
fromLayer: v.fromLayer,
|
|
40
|
+
toLayer: v.toLayer,
|
|
41
|
+
target: v.target,
|
|
42
|
+
message: v.message,
|
|
43
|
+
};
|
|
44
|
+
const verdict = classifyRemediation(shaped);
|
|
45
|
+
if (verdict.class === 'mechanical-safe') continue;
|
|
46
|
+
const enriched = enrichViolationWithFixClass(shaped);
|
|
47
|
+
return {
|
|
48
|
+
fixClass: enriched.fixClass,
|
|
49
|
+
decision: enriched.enthusiastHint,
|
|
50
|
+
remediationClass: verdict.class,
|
|
51
|
+
...(verdict.remediationKind ? { remediationKind: verdict.remediationKind } : {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// All mechanical-safe or unclassifiable — still offer first enriched hint
|
|
55
|
+
const first = violations[0];
|
|
56
|
+
const ruleId = first.ruleId || first.code;
|
|
57
|
+
const shaped = { ...first, ruleId };
|
|
58
|
+
const enriched = enrichViolationWithFixClass(shaped);
|
|
59
|
+
const verdict = classifyRemediation(shaped);
|
|
60
|
+
return {
|
|
61
|
+
fixClass: enriched.fixClass,
|
|
62
|
+
decision: enriched.enthusiastHint,
|
|
63
|
+
remediationClass: verdict.class,
|
|
64
|
+
...(verdict.remediationKind ? { remediationKind: verdict.remediationKind } : {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Compose placement + write-boundary validation into one prepare_write result.
|
|
70
|
+
*
|
|
71
|
+
* @param {{
|
|
72
|
+
* source: string,
|
|
73
|
+
* placement: object,
|
|
74
|
+
* root: string,
|
|
75
|
+
* ts: object,
|
|
76
|
+
* validate: (source: string) => { valid: boolean, violations?: any[] },
|
|
77
|
+
* resolveTargetAbs?: Function,
|
|
78
|
+
* }} opts
|
|
79
|
+
*/
|
|
80
|
+
export function composePrepareWrite(opts) {
|
|
81
|
+
const { source, placement, root, ts, validate, resolveTargetAbs } = opts;
|
|
82
|
+
if (typeof source !== 'string') {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
error: 'source is required (string)',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const identity = contentIdentity(source);
|
|
89
|
+
const filePath = placement?.filePath;
|
|
90
|
+
const gate = validateWithAutoPatch({
|
|
91
|
+
source,
|
|
92
|
+
filePath,
|
|
93
|
+
root,
|
|
94
|
+
ts,
|
|
95
|
+
validate,
|
|
96
|
+
resolveTargetAbs,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// judgmentBrief when invalid and no mechanical autoPatch (agent must decide).
|
|
100
|
+
// When autoPatch is present, omit brief — host should apply the patch first.
|
|
101
|
+
const judgment =
|
|
102
|
+
!gate.valid && !gate.autoPatch ? buildJudgmentBrief(gate.violations) : null;
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
ok: true,
|
|
106
|
+
filePath: placement?.filePath ?? null,
|
|
107
|
+
layer: placement?.layer ?? null,
|
|
108
|
+
governed: placement?.governed,
|
|
109
|
+
proposed: placement?.proposed,
|
|
110
|
+
mayImport: placement?.mayImport,
|
|
111
|
+
mustNotImport: placement?.mustNotImport,
|
|
112
|
+
forbiddenGlobals: placement?.forbiddenGlobals ?? [],
|
|
113
|
+
...(placement?.mayImportInfrastructure ? { mayImportInfrastructure: true } : {}),
|
|
114
|
+
...(placement?.suggestedLayers ? { suggestedLayers: placement.suggestedLayers } : {}),
|
|
115
|
+
...(placement?.message ? { placementMessage: placement.message } : {}),
|
|
116
|
+
...(placement?.note ? { placementNote: placement.note } : {}),
|
|
117
|
+
...(placement?.description ? { description: placement.description } : {}),
|
|
118
|
+
valid: gate.valid,
|
|
119
|
+
violations: gate.violations,
|
|
120
|
+
...(gate.autoPatch ? { autoPatch: gate.autoPatch } : {}),
|
|
121
|
+
...(judgment ? { judgmentBrief: judgment } : {}),
|
|
122
|
+
contentHash: identity.contentHash,
|
|
123
|
+
byteLength: identity.byteLength,
|
|
124
|
+
...(gate.autoPatch
|
|
125
|
+
? {
|
|
126
|
+
autoPatchContentHash: contentIdentity(gate.autoPatch.source).contentHash,
|
|
127
|
+
}
|
|
128
|
+
: {}),
|
|
129
|
+
};
|
|
130
|
+
}
|
package/bin/lib/remediation.mjs
CHANGED
|
@@ -19,6 +19,14 @@ export const MECHANICAL_SAFE_KINDS = [
|
|
|
19
19
|
'type-only-import-move',
|
|
20
20
|
'import-type-from-pure-type-module',
|
|
21
21
|
'import-type-of-type-exports',
|
|
22
|
+
// port-proof-inject-binding is intentionally NOT mechanical-safe (signature change).
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Judgment-class kinds that still have a named transform / plan label (eval corpus vocabulary).
|
|
26
|
+
* Never auto-apply without multi-file / caller proof.
|
|
27
|
+
*/
|
|
28
|
+
export const JUDGMENT_SUGGESTED_KINDS = [
|
|
29
|
+
'port-proof-inject-binding',
|
|
22
30
|
];
|
|
23
31
|
/** fixClass values from enrichViolationWithFixClass (eval corpus / reports). */
|
|
24
32
|
export const KNOWN_FIX_CLASSES = [
|
|
@@ -91,6 +99,19 @@ export function classifyRemediation(violation) {
|
|
|
91
99
|
rationale: 'Named bindings are type-only exports of the target module (even if the file also exports values): convert to `import type` / `export type` (erased at runtime). Gate verifies.',
|
|
92
100
|
};
|
|
93
101
|
}
|
|
102
|
+
// W6: port-proof inject is a *suggested* shape when proof holds, but always judgment
|
|
103
|
+
// for auto-apply — adding a required parameter breaks external call sites.
|
|
104
|
+
if (violation?.portProofEligible &&
|
|
105
|
+
edgeKind !== 'require' &&
|
|
106
|
+
edgeKind !== 'dynamic-import' &&
|
|
107
|
+
!violation?.typeOnly) {
|
|
108
|
+
return {
|
|
109
|
+
class: 'judgment',
|
|
110
|
+
confidence: 0.82,
|
|
111
|
+
remediationKind: 'port-proof-inject-binding',
|
|
112
|
+
rationale: 'Port-proof shape: single named value import used only as binding.method(...) in function declarations. Inject as a port parameter (body-local calls preserved) — outer layer must pass the impl. Not mechanical-safe auto-apply: call arity changes. Apply via agent judgment / multi-file plan.',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
94
115
|
return {
|
|
95
116
|
class: 'judgment',
|
|
96
117
|
confidence: 0.7,
|
package/dist/index.cjs
CHANGED
|
@@ -80,7 +80,7 @@ __export(index_exports, {
|
|
|
80
80
|
module.exports = __toCommonJS(index_exports);
|
|
81
81
|
|
|
82
82
|
// src/version.ts
|
|
83
|
-
var version = "2.
|
|
83
|
+
var version = "2.10.0";
|
|
84
84
|
|
|
85
85
|
// src/kernel/intent/IntentRegistry.ts
|
|
86
86
|
var IntentRegistry = class {
|
|
@@ -2255,7 +2255,9 @@ function extractModuleSpecifiers(source) {
|
|
|
2255
2255
|
let match;
|
|
2256
2256
|
while ((match = pattern.re.exec(source)) !== null) {
|
|
2257
2257
|
const index = match.index + match[0].indexOf(match[1]);
|
|
2258
|
-
|
|
2258
|
+
const raw = match[0];
|
|
2259
|
+
const typeOnly = pattern.kind === "import" && /\bimport\s+type\b/.test(raw) || pattern.kind === "export" && /\bexport\s+type\b/.test(raw);
|
|
2260
|
+
matches.push({ value: match[1], index, kind: pattern.kind, typeOnly });
|
|
2259
2261
|
}
|
|
2260
2262
|
}
|
|
2261
2263
|
return matches.sort((a, b) => a.index - b.index);
|
|
@@ -2495,6 +2497,9 @@ function createAICodeGate(options = {}) {
|
|
|
2495
2497
|
}
|
|
2496
2498
|
);
|
|
2497
2499
|
if (blocked) {
|
|
2500
|
+
if (specifier.typeOnly && !blocked.peerIsolation) {
|
|
2501
|
+
continue;
|
|
2502
|
+
}
|
|
2498
2503
|
const peer = Boolean(blocked.peerIsolation);
|
|
2499
2504
|
violations.push(
|
|
2500
2505
|
violation(
|
|
@@ -2508,7 +2513,11 @@ function createAICodeGate(options = {}) {
|
|
|
2508
2513
|
fromLayer: contextLayer,
|
|
2509
2514
|
toLayer: targetLayer,
|
|
2510
2515
|
suggestion: peer ? "Extract shared code to a shared layer, or coordinate slices via events/ports \u2014 do not import across feature/context slices." : "Depend on a port/interface owned by an inner layer instead, or move this code to a layer allowed to make this import.",
|
|
2511
|
-
details: {
|
|
2516
|
+
details: {
|
|
2517
|
+
importKind: specifier.kind,
|
|
2518
|
+
peerIsolation: peer,
|
|
2519
|
+
...specifier.typeOnly ? { typeOnly: true } : {}
|
|
2520
|
+
}
|
|
2512
2521
|
}
|
|
2513
2522
|
)
|
|
2514
2523
|
);
|
|
@@ -2518,7 +2527,7 @@ function createAICodeGate(options = {}) {
|
|
|
2518
2527
|
continue;
|
|
2519
2528
|
}
|
|
2520
2529
|
}
|
|
2521
|
-
if (exemptFromInfraHeuristics) continue;
|
|
2530
|
+
if (exemptFromInfraHeuristics || specifier.typeOnly) continue;
|
|
2522
2531
|
if (!hasInfrastructureToken(specifier.value) && !isKnownInfrastructurePackage(specifier.value)) {
|
|
2523
2532
|
continue;
|
|
2524
2533
|
}
|