cabloy 5.1.192 → 5.1.193
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/.cabloy-version +1 -1
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/repo-agent-governance/adapters/claude/contract-loop-gate.ts +15 -187
- package/repo-agent-governance/adapters/codex/contract-loop-gate.mjs +25 -0
- package/repo-agent-governance/adapters/codex/hooks.json +19 -0
- package/repo-agent-governance/adapters/cursor/contract-loop-gate.mjs +28 -0
- package/repo-agent-governance/adapters/cursor/hooks.json +19 -0
- package/repo-agent-governance/managed-assets.json +38 -10
- package/repo-agent-governance/manifest.json +24 -0
- package/repo-agent-governance/policies/repository.md +1 -1
- package/repo-agent-governance/scripts/governance.mjs +101 -2
- package/repo-agent-governance/scripts/pack-check.mjs +5 -0
- package/repo-agent-governance/skills/cabloy-contract-loop/SKILL.md +9 -2
- package/repo-agent-governance/skills/cabloy-contract-loop/references/verification-checklist.md +5 -3
- package/repo-agent-governance/tests/governance.test.mjs +274 -0
- package/repo-agent-governance/tools/contract-loop/core.mjs +30 -14
- package/repo-agent-governance/tools/contract-loop/hook-runtime.mjs +367 -0
- package/repo-docs/ai/agent-governance.md +8 -6
- package/repo-docs/ai/docs-skills-rules-mapping.md +4 -1
- package/repo-docs/ai/edition-consistency-checklist.md +1 -1
- package/repo-docs/ai/introduction.md +1 -1
- package/repo-docs/ai/repo-guidance.md +1 -1
- package/repo-docs/ai/rules-and-config.md +5 -3
- package/repo-docs/reference/package-map.md +1 -1
|
@@ -30,7 +30,14 @@ Do not keep debugging source-level contract or renderer changes until the local
|
|
|
30
30
|
## Current safeguard behavior in this repo
|
|
31
31
|
|
|
32
32
|
- there is no contract-loop pre-commit gate in the current repo workflow
|
|
33
|
-
- the active
|
|
33
|
+
- the active safeguards are platform-specific adapters over the same portable classifier and reverse auto-sync runtime in `repo-agent-governance/tools/contract-loop/`
|
|
34
|
+
- Claude Code uses the `PostToolUse` hook in `.claude/settings.json`
|
|
35
|
+
- Codex uses the `PostToolUse` hook in `.codex/hooks.json`; it matches `apply_patch` / `Edit` / `Write`, resolves the project-local bridge without requiring Git, preserves Add/Update/Delete patch operations, and returns model-visible context through the Codex hook contract
|
|
36
|
+
- Codex project-hook discovery and trust remain client-local. In a non-Git project opened from a nested directory, configure the local Codex `project_root_markers` to include `__CABLOY_BASIC__` and `__CABLOY_START__` alongside `.git`; the repository must not write that user setting
|
|
37
|
+
- Cursor uses native `postToolUse` for Agent `Write` context plus `afterTabFileEdit` for Tab `TabWrite` side effects in `.cursor/hooks.json`; the native `afterFileEdit` route is intentionally absent so one Agent write does not run the gate twice
|
|
38
|
+
- Cursor payloads are not Claude hook payloads. When Cursor imports `.claude/settings.json`, the Claude bridge recognizes Cursor and returns without work, leaving the native Cursor adapter as the single Agent gate
|
|
39
|
+
- `postToolUse` injects `additional_context` into the agent turn; `afterTabFileEdit` is side-effect only and still runs the shared reverse auto-sync when the edited file matches
|
|
40
|
+
- Codex handles every matching entry in one `apply_patch` call and runs at most one high-confidence reverse auto-sync for that patch. Explicit Delete entries use path-only evidence; an unreadable Add or Update target is reported as an inspection failure, never reinterpreted as a delete
|
|
34
41
|
- for high-confidence reverse-chain source edits on the Zova side, the hook auto-runs `npm run build:zova:admin` and then `npm run deps:vona`
|
|
35
42
|
- forward-chain detections remain reminder-only, so backend contract changes still require deliberate regeneration and verification
|
|
36
43
|
- consumer-side reverse signals remain reminder-only, so do not assume every reverse-chain case auto-syncs itself
|
|
@@ -238,7 +245,7 @@ Important Cabloy Basic reverse-sync rule:
|
|
|
238
245
|
- run `npm run build:zova:admin` from the repo root instead, then run `npm run deps:vona`
|
|
239
246
|
- treat this as a JS-bundle-plus-rest-output handoff, not a rest-types-only refresh
|
|
240
247
|
- the current repo safeguard may auto-run those two commands for high-confidence Zova reverse-source edits, but only as a convenience layer on top of the contract-loop model
|
|
241
|
-
- if the change was consumer-side, low-confidence, cross-edition,
|
|
248
|
+
- if the change was consumer-side, low-confidence, cross-edition, happened outside a supported platform hook path, or used an untrusted/discovery-disabled hook, run the reverse sync flow deliberately yourself
|
|
242
249
|
- prefer visible proof under `zova/src/**/.metadata/**` when it is available; if the effective handoff only appears in `.zova-rest`, treat the safeguard as conservative reminder/auto-sync assistance rather than strict proof
|
|
243
250
|
|
|
244
251
|
For Cabloy Start, verify the exact Start-specific flavor names, paths, SSR site baselines, project assets, and source-confirmed root wrappers in the active Start repository. If the work affects an independent SSR site, follow Mode E: build that site’s paired wrapper rather than assuming an Admin/Web command covers it.
|
package/repo-agent-governance/skills/cabloy-contract-loop/references/verification-checklist.md
CHANGED
|
@@ -42,9 +42,11 @@ When one persisted domain has both Admin Resource and Web self-service consumers
|
|
|
42
42
|
- `build:rest:*` was not treated as the only Vona SSR handoff
|
|
43
43
|
- backend consumers can resolve the refreshed frontend-generated handoff
|
|
44
44
|
- prefer visible proof under `zova/src/**/.metadata/**` when it is available
|
|
45
|
-
- this repo does not rely on a contract-loop pre-commit gate;
|
|
46
|
-
- if the change was a high-confidence Zova reverse-source edit through
|
|
47
|
-
-
|
|
45
|
+
- this repo does not rely on a contract-loop pre-commit gate; Claude Code, Codex, and Cursor each use a provider-specific adapter over the same classifier
|
|
46
|
+
- if the change was a high-confidence Zova reverse-source edit through a supported hook path, confirm whether the hook already auto-ran `npm run build:zova:admin` and `npm run deps:vona`
|
|
47
|
+
- Codex evaluates an `apply_patch` batch as operation-aware entries: an explicit Delete is classified from reliable path evidence only; an unreadable Add or Update target is an inspection failure rather than a synthetic delete
|
|
48
|
+
- Cursor Agent writes use native `postToolUse` context and Cursor Tab writes use native `afterTabFileEdit` side effects; the imported Claude bridge is intentionally inactive in Cursor to avoid duplicate work
|
|
49
|
+
- if the change was consumer-side, low-confidence, outside a supported hook path, affected an untrusted/discovery-disabled hook, or was in another edition branch, run the reverse sync flow manually instead of assuming it already happened
|
|
48
50
|
- if the real handoff only appears in `.zova-rest`, treat the safeguard as conservative reminder/auto-sync assistance rather than strict proof
|
|
49
51
|
- `npm run tsc:zova`
|
|
50
52
|
- relevant flavor-specific or route-specific checks
|
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
isCodeFile,
|
|
21
21
|
resolveEdition,
|
|
22
22
|
} from '../tools/contract-loop/core.mjs';
|
|
23
|
+
import {
|
|
24
|
+
extractCodexEditedFiles,
|
|
25
|
+
extractCodexEditedFilePaths,
|
|
26
|
+
} from '../tools/contract-loop/hook-runtime.mjs';
|
|
23
27
|
|
|
24
28
|
function createFixture() {
|
|
25
29
|
const root = mkdtempSync(resolve(tmpdir(), 'cabloy-agent-governance-'));
|
|
@@ -47,6 +51,40 @@ function runGovernance(root, ...args) {
|
|
|
47
51
|
return runNode(root, resolve(root, 'repo-agent-governance/scripts/governance.mjs'), ...args);
|
|
48
52
|
}
|
|
49
53
|
|
|
54
|
+
function runHook(root, executable, payload, { cwd = root, env = {} } = {}) {
|
|
55
|
+
const result = spawnSync(process.execPath, [executable], {
|
|
56
|
+
cwd,
|
|
57
|
+
encoding: 'utf8',
|
|
58
|
+
input: JSON.stringify(payload),
|
|
59
|
+
env: {
|
|
60
|
+
...process.env,
|
|
61
|
+
CABLOY_GOVERNANCE_ROOT: root,
|
|
62
|
+
...env,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
status: result.status,
|
|
67
|
+
output: `${result.stdout}${result.stderr}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function runShellHook(root, command, payload, cwd = root) {
|
|
72
|
+
const result = spawnSync(command, {
|
|
73
|
+
shell: true,
|
|
74
|
+
cwd,
|
|
75
|
+
encoding: 'utf8',
|
|
76
|
+
input: JSON.stringify(payload),
|
|
77
|
+
env: {
|
|
78
|
+
...process.env,
|
|
79
|
+
CABLOY_GOVERNANCE_ROOT: root,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
status: result.status,
|
|
84
|
+
output: `${result.stdout}${result.stderr}`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
50
88
|
function readState(root) {
|
|
51
89
|
return JSON.parse(readFileSync(resolve(root, '.cabloy-agent-governance-state.json'), 'utf8'));
|
|
52
90
|
}
|
|
@@ -63,6 +101,13 @@ function removePolicyTarget(root, target) {
|
|
|
63
101
|
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
64
102
|
}
|
|
65
103
|
|
|
104
|
+
function setCursorHookTarget(root, target) {
|
|
105
|
+
const manifestPath = resolve(root, 'repo-agent-governance/manifest.json');
|
|
106
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
107
|
+
manifest.cursor.hooks[0].target = target;
|
|
108
|
+
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
109
|
+
}
|
|
110
|
+
|
|
66
111
|
function withFixture(callback) {
|
|
67
112
|
const root = createFixture();
|
|
68
113
|
try {
|
|
@@ -215,6 +260,34 @@ test('governance adoption deletes only unchanged retired outputs', () => {
|
|
|
215
260
|
});
|
|
216
261
|
});
|
|
217
262
|
|
|
263
|
+
test('governance rendering removes unchanged retired outputs', () => {
|
|
264
|
+
withFixture(root => {
|
|
265
|
+
const legacyTarget = '.cursor/hooks/contract-loop-gate.ts';
|
|
266
|
+
setCursorHookTarget(root, legacyTarget);
|
|
267
|
+
assert.equal(runGovernance(root, 'render').status, 0);
|
|
268
|
+
assert.ok(existsSync(resolve(root, legacyTarget)));
|
|
269
|
+
|
|
270
|
+
setCursorHookTarget(root, '.cursor/hooks/contract-loop-gate.mjs');
|
|
271
|
+
assert.equal(runGovernance(root, 'render').status, 0);
|
|
272
|
+
assert.equal(existsSync(resolve(root, legacyTarget)), false);
|
|
273
|
+
assert.ok(existsSync(resolve(root, '.cursor/hooks/contract-loop-gate.mjs')));
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('governance rendering preserves modified retired outputs', () => {
|
|
278
|
+
withFixture(root => {
|
|
279
|
+
const legacyTarget = '.cursor/hooks/contract-loop-gate.ts';
|
|
280
|
+
setCursorHookTarget(root, legacyTarget);
|
|
281
|
+
assert.equal(runGovernance(root, 'render').status, 0);
|
|
282
|
+
const legacyPath = resolve(root, legacyTarget);
|
|
283
|
+
writeFileSync(legacyPath, 'project-owned legacy hook\n');
|
|
284
|
+
|
|
285
|
+
setCursorHookTarget(root, '.cursor/hooks/contract-loop-gate.mjs');
|
|
286
|
+
assert.equal(runGovernance(root, 'render').status, 0);
|
|
287
|
+
assert.equal(readFileSync(legacyPath, 'utf8'), 'project-owned legacy hook\n');
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
218
291
|
test('governance adoption preserves modified retired outputs', () => {
|
|
219
292
|
withFixture(root => {
|
|
220
293
|
assert.equal(runGovernance(root, 'adopt', '--apply').status, 0);
|
|
@@ -293,3 +366,204 @@ test('contract-loop recognizes supported vendor and suite source paths', () => {
|
|
|
293
366
|
assert.equal(isCodeFile('/repo/vona/src/module/demo/src/service/demo.ts'), true);
|
|
294
367
|
assert.equal(isCodeFile('/repo/repo-docs/ai/agent-governance.md'), false);
|
|
295
368
|
});
|
|
369
|
+
|
|
370
|
+
test('Codex contract-loop adapter evaluates operation-aware apply_patch paths', () => {
|
|
371
|
+
withFixture(root => {
|
|
372
|
+
const sourcePath = resolve(root, 'vona/src/module/demo/src/controller/demo.ts');
|
|
373
|
+
mkdirSync(dirname(sourcePath), { recursive: true });
|
|
374
|
+
writeFileSync(sourcePath, '@Api.field()\n');
|
|
375
|
+
assert.equal(runGovernance(root, 'render').status, 0);
|
|
376
|
+
|
|
377
|
+
const command = [
|
|
378
|
+
'*** Begin Patch',
|
|
379
|
+
'*** Update File: vona/src/module/demo/src/controller/demo.ts',
|
|
380
|
+
'@@',
|
|
381
|
+
'-@Api.field()',
|
|
382
|
+
'+@Api.field()',
|
|
383
|
+
'*** Update File: repo-docs/ai/agent-governance.md',
|
|
384
|
+
'@@',
|
|
385
|
+
'*** End Patch',
|
|
386
|
+
].join('\n');
|
|
387
|
+
assert.deepEqual(extractCodexEditedFilePaths({ tool_input: { command } }), [
|
|
388
|
+
'vona/src/module/demo/src/controller/demo.ts',
|
|
389
|
+
'repo-docs/ai/agent-governance.md',
|
|
390
|
+
]);
|
|
391
|
+
assert.deepEqual(extractCodexEditedFiles({ tool_input: { command } }), [
|
|
392
|
+
{ operation: 'update', filePath: 'vona/src/module/demo/src/controller/demo.ts' },
|
|
393
|
+
{ operation: 'update', filePath: 'repo-docs/ai/agent-governance.md' },
|
|
394
|
+
]);
|
|
395
|
+
|
|
396
|
+
const result = runHook(root, resolve(root, '.codex/hooks/contract-loop-gate.mjs'), {
|
|
397
|
+
hook_event_name: 'PostToolUse',
|
|
398
|
+
tool_name: 'apply_patch',
|
|
399
|
+
tool_input: { command },
|
|
400
|
+
});
|
|
401
|
+
assert.equal(result.status, 0, result.output);
|
|
402
|
+
const output = JSON.parse(result.output);
|
|
403
|
+
assert.equal(output.hookSpecificOutput.hookEventName, 'PostToolUse');
|
|
404
|
+
assert.match(output.hookSpecificOutput.additionalContext, /Forward chain:/);
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test('Codex launch command finds a non-Git project from nested cwd', () => {
|
|
409
|
+
withFixture(root => {
|
|
410
|
+
const sourcePath = resolve(root, 'vona/src/module/demo/src/controller/demo.ts');
|
|
411
|
+
mkdirSync(dirname(sourcePath), { recursive: true });
|
|
412
|
+
writeFileSync(sourcePath, '@Api.field()\n');
|
|
413
|
+
assert.equal(runGovernance(root, 'render').status, 0);
|
|
414
|
+
|
|
415
|
+
const hook = JSON.parse(readFileSync(resolve(root, '.codex/hooks.json'), 'utf8')).hooks
|
|
416
|
+
.PostToolUse[0].hooks[0];
|
|
417
|
+
assert.doesNotMatch(hook.command, /git rev-parse|\$\(/);
|
|
418
|
+
assert.equal(hook.commandWindows, hook.command);
|
|
419
|
+
|
|
420
|
+
const command = [
|
|
421
|
+
'*** Begin Patch',
|
|
422
|
+
'*** Update File: src/controller/demo.ts',
|
|
423
|
+
'@@',
|
|
424
|
+
'-@Api.field()',
|
|
425
|
+
'+@Api.field()',
|
|
426
|
+
'*** End Patch',
|
|
427
|
+
].join('\n');
|
|
428
|
+
const nestedCwd = resolve(root, 'vona/src/module/demo');
|
|
429
|
+
const result = runShellHook(
|
|
430
|
+
root,
|
|
431
|
+
hook.command,
|
|
432
|
+
{
|
|
433
|
+
hook_event_name: 'PostToolUse',
|
|
434
|
+
tool_name: 'apply_patch',
|
|
435
|
+
cwd: nestedCwd,
|
|
436
|
+
tool_input: { command },
|
|
437
|
+
},
|
|
438
|
+
nestedCwd,
|
|
439
|
+
);
|
|
440
|
+
assert.equal(result.status, 0, result.output);
|
|
441
|
+
assert.match(JSON.parse(result.output).hookSpecificOutput.additionalContext, /Forward chain:/);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test('Codex delete paths are classified without reading removed sources', () => {
|
|
446
|
+
withFixture(root => {
|
|
447
|
+
assert.equal(runGovernance(root, 'render').status, 0);
|
|
448
|
+
const command = [
|
|
449
|
+
'*** Begin Patch',
|
|
450
|
+
'*** Delete File: vona/src/module/demo/src/controller/demo.ts',
|
|
451
|
+
'*** Delete File: zova/src/module/demo/src/bean/demo.ts',
|
|
452
|
+
'*** Delete File: repo-docs/ai/agent-governance.md',
|
|
453
|
+
'*** End Patch',
|
|
454
|
+
].join('\n');
|
|
455
|
+
const result = runHook(root, resolve(root, '.codex/hooks/contract-loop-gate.mjs'), {
|
|
456
|
+
hook_event_name: 'PostToolUse',
|
|
457
|
+
tool_name: 'apply_patch',
|
|
458
|
+
tool_input: { command },
|
|
459
|
+
});
|
|
460
|
+
assert.equal(result.status, 0, result.output);
|
|
461
|
+
const output = JSON.parse(result.output).hookSpecificOutput.additionalContext;
|
|
462
|
+
assert.match(output, /Forward chain:/);
|
|
463
|
+
assert.match(output, /Reverse chain:/);
|
|
464
|
+
assert.match(output, /Auto-sync failed during `npm run build:zova:admin`/);
|
|
465
|
+
assert.doesNotMatch(output, /agent-governance\.md/);
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test('Codex missing update does not become a delete and does not conceal a delete', () => {
|
|
470
|
+
withFixture(root => {
|
|
471
|
+
assert.equal(runGovernance(root, 'render').status, 0);
|
|
472
|
+
const command = [
|
|
473
|
+
'*** Begin Patch',
|
|
474
|
+
'*** Update File: vona/src/module/demo/src/controller/missing.ts',
|
|
475
|
+
'@@',
|
|
476
|
+
'*** Delete File: vona/src/module/demo/src/dto/deleted.ts',
|
|
477
|
+
'*** End Patch',
|
|
478
|
+
].join('\n');
|
|
479
|
+
const result = runHook(root, resolve(root, '.codex/hooks/contract-loop-gate.mjs'), {
|
|
480
|
+
hook_event_name: 'PostToolUse',
|
|
481
|
+
tool_name: 'apply_patch',
|
|
482
|
+
tool_input: { command },
|
|
483
|
+
});
|
|
484
|
+
assert.equal(result.status, 0, result.output);
|
|
485
|
+
const output = JSON.parse(result.output).hookSpecificOutput.additionalContext;
|
|
486
|
+
assert.match(output, /could not inspect edited source/);
|
|
487
|
+
assert.match(output, /Forward chain:/);
|
|
488
|
+
assert.doesNotMatch(output, /Reverse chain:/);
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test('Codex move paths inspect the destination', () => {
|
|
493
|
+
withFixture(root => {
|
|
494
|
+
const destinationPath = resolve(root, 'vona/src/module/demo/src/controller/moved.ts');
|
|
495
|
+
mkdirSync(dirname(destinationPath), { recursive: true });
|
|
496
|
+
writeFileSync(destinationPath, '@Api.field()\n');
|
|
497
|
+
assert.equal(runGovernance(root, 'render').status, 0);
|
|
498
|
+
const command = [
|
|
499
|
+
'*** Begin Patch',
|
|
500
|
+
'*** Update File: vona/src/module/demo/src/service/old.ts',
|
|
501
|
+
'*** Move to: vona/src/module/demo/src/controller/moved.ts',
|
|
502
|
+
'@@',
|
|
503
|
+
'*** End Patch',
|
|
504
|
+
].join('\n');
|
|
505
|
+
assert.deepEqual(extractCodexEditedFiles({ tool_input: { command } }), [
|
|
506
|
+
{ operation: 'update', filePath: 'vona/src/module/demo/src/controller/moved.ts' },
|
|
507
|
+
]);
|
|
508
|
+
const result = runHook(root, resolve(root, '.codex/hooks/contract-loop-gate.mjs'), {
|
|
509
|
+
hook_event_name: 'PostToolUse',
|
|
510
|
+
tool_name: 'apply_patch',
|
|
511
|
+
tool_input: { command },
|
|
512
|
+
});
|
|
513
|
+
assert.equal(result.status, 0, result.output);
|
|
514
|
+
assert.match(JSON.parse(result.output).hookSpecificOutput.additionalContext, /Forward chain:/);
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test('Cursor contract-loop adapter handles Agent and Tab edit events once each', () => {
|
|
519
|
+
withFixture(root => {
|
|
520
|
+
const sourcePath = resolve(root, 'vona/src/module/demo/src/controller/demo.ts');
|
|
521
|
+
mkdirSync(dirname(sourcePath), { recursive: true });
|
|
522
|
+
writeFileSync(sourcePath, '@Api.field()\n');
|
|
523
|
+
assert.equal(runGovernance(root, 'render').status, 0);
|
|
524
|
+
|
|
525
|
+
const hooks = JSON.parse(readFileSync(resolve(root, '.cursor/hooks.json'), 'utf8'));
|
|
526
|
+
assert.equal(hooks.hooks.postToolUse[0].matcher, 'Write');
|
|
527
|
+
assert.equal(hooks.hooks.afterFileEdit, undefined);
|
|
528
|
+
assert.equal(hooks.hooks.afterTabFileEdit[0].matcher, 'TabWrite');
|
|
529
|
+
|
|
530
|
+
const script = resolve(root, '.cursor/hooks/contract-loop-gate.mjs');
|
|
531
|
+
const tabResult = runHook(root, script, {
|
|
532
|
+
hook_event_name: 'afterTabFileEdit',
|
|
533
|
+
file_path: sourcePath,
|
|
534
|
+
});
|
|
535
|
+
assert.equal(tabResult.status, 0, tabResult.output);
|
|
536
|
+
assert.equal(tabResult.output, '');
|
|
537
|
+
|
|
538
|
+
const result = runHook(root, script, {
|
|
539
|
+
hook_event_name: 'postToolUse',
|
|
540
|
+
tool_input: { file_path: sourcePath },
|
|
541
|
+
});
|
|
542
|
+
assert.equal(result.status, 0, result.output);
|
|
543
|
+
assert.match(JSON.parse(result.output).additional_context, /Forward chain:/);
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test('Claude contract-loop adapter stays inert when imported by Cursor', () => {
|
|
548
|
+
withFixture(root => {
|
|
549
|
+
const sourcePath = resolve(root, 'vona/src/module/demo/src/controller/demo.ts');
|
|
550
|
+
mkdirSync(dirname(sourcePath), { recursive: true });
|
|
551
|
+
writeFileSync(sourcePath, '@Api.field()\n');
|
|
552
|
+
assert.equal(runGovernance(root, 'render').status, 0);
|
|
553
|
+
const script = resolve(root, '.claude/hooks/contract-loop-gate.ts');
|
|
554
|
+
const payload = { hook_event_name: 'PostToolUse', tool_input: { file_path: sourcePath } };
|
|
555
|
+
|
|
556
|
+
const claudeResult = runHook(root, script, payload);
|
|
557
|
+
assert.equal(claudeResult.status, 0, claudeResult.output);
|
|
558
|
+
assert.match(
|
|
559
|
+
JSON.parse(claudeResult.output).hookSpecificOutput.additionalContext,
|
|
560
|
+
/Forward chain:/,
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
const cursorResult = runHook(root, script, payload, {
|
|
564
|
+
env: { CURSOR_PROJECT_DIR: root },
|
|
565
|
+
});
|
|
566
|
+
assert.equal(cursorResult.status, 0, cursorResult.output);
|
|
567
|
+
assert.equal(cursorResult.output, '');
|
|
568
|
+
});
|
|
569
|
+
});
|
|
@@ -100,35 +100,51 @@ export function isCodeFile(filePath) {
|
|
|
100
100
|
);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function
|
|
104
|
-
if (
|
|
105
|
-
if (
|
|
106
|
-
pathContainsAny(filePath, FORWARD_PATH_MARKERS) ||
|
|
107
|
-
containsAny(content, FORWARD_CONTENT_MARKERS)
|
|
108
|
-
) {
|
|
103
|
+
function detectForwardPath(filePath) {
|
|
104
|
+
if (filePath.includes('/vona/src/') && pathContainsAny(filePath, FORWARD_PATH_MARKERS)) {
|
|
109
105
|
return 'Backend contract source may have changed.';
|
|
110
106
|
}
|
|
111
107
|
return null;
|
|
112
108
|
}
|
|
113
109
|
|
|
114
|
-
function
|
|
110
|
+
function detectForwardContent(filePath, content) {
|
|
111
|
+
if (filePath.includes('/vona/src/') && containsAny(content, FORWARD_CONTENT_MARKERS)) {
|
|
112
|
+
return 'Backend contract source may have changed.';
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function detectReversePath(filePath) {
|
|
118
|
+
if (filePath.includes('/zova/src/') && pathContainsAny(filePath, REVERSE_ZOVA_PATH_MARKERS)) {
|
|
119
|
+
return 'Frontend-owned resources or metadata may affect backend consumers.';
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function detectReverseContent(filePath, content, reverseVonaContentMarkers) {
|
|
115
125
|
if (filePath.includes('/vona/src/') && containsAny(content, reverseVonaContentMarkers)) {
|
|
116
126
|
return 'Vona code is consuming frontend metadata or render resources.';
|
|
117
127
|
}
|
|
118
|
-
if (
|
|
119
|
-
filePath.includes('/zova/src/') &&
|
|
120
|
-
(pathContainsAny(filePath, REVERSE_ZOVA_PATH_MARKERS) ||
|
|
121
|
-
containsAny(content, REVERSE_ZOVA_CONTENT_MARKERS))
|
|
122
|
-
) {
|
|
128
|
+
if (filePath.includes('/zova/src/') && containsAny(content, REVERSE_ZOVA_CONTENT_MARKERS)) {
|
|
123
129
|
return 'Frontend-owned resources or metadata may affect backend consumers.';
|
|
124
130
|
}
|
|
125
131
|
return null;
|
|
126
132
|
}
|
|
127
133
|
|
|
134
|
+
export function analyzePath(filePath) {
|
|
135
|
+
return {
|
|
136
|
+
forwardReason: detectForwardPath(filePath),
|
|
137
|
+
reverseReason: detectReversePath(filePath),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
128
141
|
export function analyze(filePath, content, edition) {
|
|
142
|
+
const pathResult = analyzePath(filePath);
|
|
129
143
|
return {
|
|
130
|
-
forwardReason:
|
|
131
|
-
reverseReason:
|
|
144
|
+
forwardReason: pathResult.forwardReason ?? detectForwardContent(filePath, content),
|
|
145
|
+
reverseReason:
|
|
146
|
+
pathResult.reverseReason ??
|
|
147
|
+
detectReverseContent(filePath, content, getReverseVonaContentMarkers(edition)),
|
|
132
148
|
};
|
|
133
149
|
}
|
|
134
150
|
|