@mmnto/cli 1.101.2 → 1.103.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/dist/commands/init-detect.js +1 -1
- package/dist/commands/init-detect.js.map +1 -1
- package/dist/commands/init-detect.test.js +2 -1
- package/dist/commands/init-detect.test.js.map +1 -1
- package/dist/commands/init-templates.d.ts.map +1 -1
- package/dist/commands/init-templates.js +118 -9
- package/dist/commands/init-templates.js.map +1 -1
- package/dist/commands/init.test.js +186 -5
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/review-fan.d.ts +9 -3
- package/dist/commands/review-fan.d.ts.map +1 -1
- package/dist/commands/review-fan.js +131 -16
- package/dist/commands/review-fan.js.map +1 -1
- package/dist/commands/review-fan.test.js +246 -7
- package/dist/commands/review-fan.test.js.map +1 -1
- package/dist/commands/shield-nonreview.test.d.ts +24 -0
- package/dist/commands/shield-nonreview.test.d.ts.map +1 -0
- package/dist/commands/shield-nonreview.test.js +175 -0
- package/dist/commands/shield-nonreview.test.js.map +1 -0
- package/dist/commands/shield.d.ts +33 -7
- package/dist/commands/shield.d.ts.map +1 -1
- package/dist/commands/shield.js +96 -50
- package/dist/commands/shield.js.map +1 -1
- package/dist/commands/shield.test.js +69 -2
- package/dist/commands/shield.test.js.map +1 -1
- package/dist/hook/schema.d.ts +2 -2
- package/dist/orchestrators/orchestrator.d.ts +86 -1
- package/dist/orchestrators/orchestrator.d.ts.map +1 -1
- package/dist/orchestrators/orchestrator.js +277 -9
- package/dist/orchestrators/orchestrator.js.map +1 -1
- package/dist/orchestrators/orchestrator.test.js +240 -4
- package/dist/orchestrators/orchestrator.test.js.map +1 -1
- package/dist/orchestrators/shell-orchestrator.d.ts +12 -1
- package/dist/orchestrators/shell-orchestrator.d.ts.map +1 -1
- package/dist/orchestrators/shell-orchestrator.js +256 -49
- package/dist/orchestrators/shell-orchestrator.js.map +1 -1
- package/dist/orchestrators/shell-orchestrator.test.js +252 -4
- package/dist/orchestrators/shell-orchestrator.test.js.map +1 -1
- package/dist/utils.d.ts +18 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +389 -108
- package/dist/utils.js.map +1 -1
- package/dist/utils.test.js +406 -14
- package/dist/utils.test.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-level deterministic-skip NON-REVIEW tests (mmnto-ai/totem#2466).
|
|
3
|
+
*
|
|
4
|
+
* `totem review` has three deterministic skip paths that drop the ENTIRE diff
|
|
5
|
+
* without examining it: all-non-code, no-code-after-filtering, and all-generated.
|
|
6
|
+
* Each used to call `writeReviewedContentHash(...)` before returning, minting the
|
|
7
|
+
* `.reviewed-content-hash` push-gate stamp for content nothing reviewed.
|
|
8
|
+
*
|
|
9
|
+
* The danger is not uniform, which is why all three are pinned here rather than
|
|
10
|
+
* only the reported one:
|
|
11
|
+
*
|
|
12
|
+
* - all-non-code / filtered-empty fire only when no code file is in the diff,
|
|
13
|
+
* so the hash is unchanged and the stamp was a no-op. The defect is the
|
|
14
|
+
* dishonest clean-pass surface (Tenets 4/13).
|
|
15
|
+
* - all-generated can drop a TRACKED, HASHED source file, because
|
|
16
|
+
* `.gitattributes linguist-generated` can mark a `.ts` as generated. There the
|
|
17
|
+
* stamp genuinely authorized never-reviewed code — a push-gate bypass.
|
|
18
|
+
*
|
|
19
|
+
* Mirrors `shield-covariate.test.ts`: its own file to avoid mock contamination,
|
|
20
|
+
* mocking the heavy seams (config, engine bootstrap, hook installer, git diff) so
|
|
21
|
+
* the skip branches are exercised without a real repo, config, or network invoke.
|
|
22
|
+
*/
|
|
23
|
+
import * as fs from 'node:fs';
|
|
24
|
+
import * as os from 'node:os';
|
|
25
|
+
import * as path from 'node:path';
|
|
26
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
27
|
+
import { cleanTmpDir } from '../test-utils.js';
|
|
28
|
+
// ── Seams that MUST NOT run once a skip path is taken ──
|
|
29
|
+
const bootstrapEngineSpy = vi.fn(async (..._args) => { });
|
|
30
|
+
const upgradePrePushHookSpy = vi.fn((..._args) => false);
|
|
31
|
+
const getDiffForReviewSpy = vi.fn(async (..._args) => null);
|
|
32
|
+
const TEST_CONFIG = {
|
|
33
|
+
totemDir: '.totem',
|
|
34
|
+
review: { sourceExtensions: ['.ts'] },
|
|
35
|
+
};
|
|
36
|
+
vi.mock('../utils/bootstrap-engine.js', async (importOriginal) => {
|
|
37
|
+
const actual = await importOriginal();
|
|
38
|
+
return { ...actual, bootstrapEngine: bootstrapEngineSpy };
|
|
39
|
+
});
|
|
40
|
+
vi.mock('./install-hooks.js', async (importOriginal) => {
|
|
41
|
+
const actual = await importOriginal();
|
|
42
|
+
return { ...actual, upgradePrePushHookIfNeeded: upgradePrePushHookSpy };
|
|
43
|
+
});
|
|
44
|
+
vi.mock('../utils.js', async (importOriginal) => {
|
|
45
|
+
const actual = await importOriginal();
|
|
46
|
+
return {
|
|
47
|
+
...actual,
|
|
48
|
+
loadEnv: vi.fn(),
|
|
49
|
+
resolveConfigPath: (cwd) => path.join(cwd, 'totem.config.ts'),
|
|
50
|
+
loadConfig: vi.fn(async () => TEST_CONFIG),
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
vi.mock('../git.js', async (importOriginal) => {
|
|
54
|
+
const actual = await importOriginal();
|
|
55
|
+
return { ...actual, getDiffForReview: getDiffForReviewSpy };
|
|
56
|
+
});
|
|
57
|
+
/** A minimal single-file diff section for `file`. */
|
|
58
|
+
function diffFor(file) {
|
|
59
|
+
return [
|
|
60
|
+
`diff --git a/${file} b/${file}`,
|
|
61
|
+
'index 1111111..2222222 100644',
|
|
62
|
+
`--- a/${file}`,
|
|
63
|
+
`+++ b/${file}`,
|
|
64
|
+
'@@ -1 +1 @@',
|
|
65
|
+
'-old',
|
|
66
|
+
'+new',
|
|
67
|
+
].join('\n');
|
|
68
|
+
}
|
|
69
|
+
describe('deterministic skip paths are NON-REVIEWS and never stamp (#2466)', () => {
|
|
70
|
+
let tmpDir;
|
|
71
|
+
let warnings;
|
|
72
|
+
const stampPath = () => path.join(tmpDir, '.totem', 'cache', '.reviewed-content-hash');
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'shield-nonreview-'));
|
|
75
|
+
warnings = [];
|
|
76
|
+
vi.spyOn(process, 'cwd').mockReturnValue(tmpDir);
|
|
77
|
+
vi.spyOn(console, 'error').mockImplementation((...args) => {
|
|
78
|
+
warnings.push(args.map(String).join(' '));
|
|
79
|
+
});
|
|
80
|
+
vi.spyOn(console, 'log').mockImplementation((...args) => {
|
|
81
|
+
warnings.push(args.map(String).join(' '));
|
|
82
|
+
});
|
|
83
|
+
bootstrapEngineSpy.mockClear();
|
|
84
|
+
getDiffForReviewSpy.mockClear();
|
|
85
|
+
});
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
vi.restoreAllMocks();
|
|
88
|
+
// The non-skip case (mixed diff) runs into the LLM path and aborts on the
|
|
89
|
+
// unmocked embedding config, which can leave a handle open under the temp
|
|
90
|
+
// dir — Windows then EPERMs the recursive remove. That is a harness artifact
|
|
91
|
+
// of driving the real command, not a product behavior, and it must not fail
|
|
92
|
+
// a suite whose assertions are all about the stamp and the emitted output.
|
|
93
|
+
try {
|
|
94
|
+
cleanTmpDir(tmpDir);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Best-effort cleanup; the OS reclaims the temp dir.
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
/**
|
|
101
|
+
* Drive the real command with a diff whose files are all `files`.
|
|
102
|
+
*
|
|
103
|
+
* A run that does NOT take a skip path continues into the LLM review path and
|
|
104
|
+
* throws on the unmocked embedding config. That throw is expected and is itself
|
|
105
|
+
* evidence the skip did not fire, so it is swallowed here — every assertion
|
|
106
|
+
* below is made on the stamp and the emitted output, never on resolution.
|
|
107
|
+
*/
|
|
108
|
+
async function runWithDiff(files) {
|
|
109
|
+
await runWithDiffRaw(files.map(diffFor).join('\n'), files);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Lower-level driver for cases where the diff BODY and the changed-file LIST
|
|
113
|
+
* must differ — the filtered-empty path needs a code file present in the list
|
|
114
|
+
* whose hunks are absent from the diff (a mode-only change has this shape).
|
|
115
|
+
*/
|
|
116
|
+
async function runWithDiffRaw(diff, changedFiles) {
|
|
117
|
+
getDiffForReviewSpy.mockResolvedValueOnce({ diff, changedFiles });
|
|
118
|
+
const { shieldCommand } = await import('./shield.js');
|
|
119
|
+
try {
|
|
120
|
+
await shieldCommand({});
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// See doc comment — a non-skip run is expected to fail downstream.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
it('all-non-code: does not stamp, and says so', async () => {
|
|
127
|
+
await runWithDiff(['docs/plan.md', 'README.md']);
|
|
128
|
+
// The load-bearing assertion: no push authorization was minted.
|
|
129
|
+
expect(fs.existsSync(stampPath())).toBe(false);
|
|
130
|
+
// And the skip is LOUD — a caller cannot read it as a clean review.
|
|
131
|
+
expect(warnings.join('\n')).toMatch(/NON-REVIEW/);
|
|
132
|
+
expect(warnings.join('\n')).toMatch(/does not authorize a push/);
|
|
133
|
+
});
|
|
134
|
+
it('all-generated: does not stamp — the path that could authorize real code', async () => {
|
|
135
|
+
// A lockfile is generated by default glob, so the whole diff drops.
|
|
136
|
+
await runWithDiff(['pnpm-lock.yaml']);
|
|
137
|
+
expect(fs.existsSync(stampPath())).toBe(false);
|
|
138
|
+
expect(warnings.join('\n')).toMatch(/NON-REVIEW/);
|
|
139
|
+
expect(warnings.join('\n')).toMatch(/does not authorize a push/);
|
|
140
|
+
});
|
|
141
|
+
it('all-generated via .gitattributes on a TRACKED .ts: does not stamp (the bypass case)', async () => {
|
|
142
|
+
// The scenario the default-glob lockfile test does NOT reach, and the only
|
|
143
|
+
// one where the old stamp could authorize genuinely-changed, hashed source:
|
|
144
|
+
// `linguist-generated` marks a real `.ts`, so the file is dropped from review
|
|
145
|
+
// while still counting toward the content hash the push gate compares.
|
|
146
|
+
fs.writeFileSync(path.join(tmpDir, '.gitattributes'), 'src/generated-client.ts linguist-generated\n');
|
|
147
|
+
await runWithDiff(['src/generated-client.ts']);
|
|
148
|
+
expect(fs.existsSync(stampPath())).toBe(false);
|
|
149
|
+
// Assert the GENERATED-path wording specifically. A bare /NON-REVIEW/ match
|
|
150
|
+
// would also pass if this fell through to the all-non-code branch, which
|
|
151
|
+
// would mean the test proves nothing about the bypass it exists to cover.
|
|
152
|
+
expect(warnings.join('\n')).toMatch(/every changed file is a generated artifact/);
|
|
153
|
+
expect(warnings.join('\n')).toMatch(/does not authorize a push/);
|
|
154
|
+
});
|
|
155
|
+
it('filtered-empty: does not stamp when no code hunks survive filtering', async () => {
|
|
156
|
+
// Stage 2 fires when the diff is neither all-code nor all-non-code and the
|
|
157
|
+
// code side contributes no hunks — a mode-only change on a tracked source
|
|
158
|
+
// file has exactly this shape: listed as changed, absent from the diff body.
|
|
159
|
+
await runWithDiffRaw(diffFor('docs/plan.md'), ['docs/plan.md', 'src/index.ts']);
|
|
160
|
+
expect(fs.existsSync(stampPath())).toBe(false);
|
|
161
|
+
// Stage-2 wording specifically — distinguishes this from the Stage-1
|
|
162
|
+
// all-non-code branch, which a generic NON-REVIEW match would not.
|
|
163
|
+
expect(warnings.join('\n')).toMatch(/no code changes remain after filtering/);
|
|
164
|
+
expect(warnings.join('\n')).toMatch(/does not authorize a push/);
|
|
165
|
+
});
|
|
166
|
+
it('mixed code + prose does NOT take a skip path (the skip must not over-fire)', async () => {
|
|
167
|
+
// Guards the inverse error: tightening the skip must not start skipping real
|
|
168
|
+
// code. A surviving `.ts` file means this is a review, not a non-review.
|
|
169
|
+
await runWithDiff(['docs/plan.md', 'src/index.ts']);
|
|
170
|
+
expect(warnings.join('\n')).not.toMatch(/NON-REVIEW/);
|
|
171
|
+
// It also must not stamp here — this run never completed a review either.
|
|
172
|
+
expect(fs.existsSync(stampPath())).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
//# sourceMappingURL=shield-nonreview.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shield-nonreview.test.js","sourceRoot":"","sources":["../../src/commands/shield-nonreview.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAIzE,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C,0DAA0D;AAC1D,MAAM,kBAAkB,GAAG,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAgB,EAAiB,EAAE,GAAE,CAAC,CAAC,CAAC;AACnF,MAAM,qBAAqB,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,KAAgB,EAAW,EAAE,CAAC,KAAK,CAAC,CAAC;AAC7E,MAAM,mBAAmB,GAAG,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAgB,EAAoB,EAAE,CAAC,IAAI,CAAC,CAAC;AAEzF,MAAM,WAAW,GAAG;IAClB,QAAQ,EAAE,QAAQ;IAClB,MAAM,EAAE,EAAE,gBAAgB,EAAE,CAAC,KAAK,CAAC,EAAE;CACZ,CAAC;AAE5B,EAAE,CAAC,IAAI,CAAC,8BAA8B,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE;IAC/D,MAAM,MAAM,GAAG,MAAM,cAAc,EAAiD,CAAC;IACrF,OAAO,EAAE,GAAG,MAAM,EAAE,eAAe,EAAE,kBAAkB,EAAE,CAAC;AAC5D,CAAC,CAAC,CAAC;AAEH,EAAE,CAAC,IAAI,CAAC,oBAAoB,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE;IACrD,MAAM,MAAM,GAAG,MAAM,cAAc,EAAuC,CAAC;IAC3E,OAAO,EAAE,GAAG,MAAM,EAAE,0BAA0B,EAAE,qBAAqB,EAAE,CAAC;AAC1E,CAAC,CAAC,CAAC;AAEH,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE;IAC9C,MAAM,MAAM,GAAG,MAAM,cAAc,EAAgC,CAAC;IACpE,OAAO;QACL,GAAG,MAAM;QACT,OAAO,EAAE,EAAE,CAAC,EAAE,EAAE;QAChB,iBAAiB,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC;QACrE,UAAU,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,WAAW,CAAC;KAC3C,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE;IAC5C,MAAM,MAAM,GAAG,MAAM,cAAc,EAA8B,CAAC;IAClE,OAAO,EAAE,GAAG,MAAM,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,CAAC;AAC9D,CAAC,CAAC,CAAC;AAEH,qDAAqD;AACrD,SAAS,OAAO,CAAC,IAAY;IAC3B,OAAO;QACL,gBAAgB,IAAI,MAAM,IAAI,EAAE;QAChC,+BAA+B;QAC/B,SAAS,IAAI,EAAE;QACf,SAAS,IAAI,EAAE;QACf,aAAa;QACb,MAAM;QACN,MAAM;KACP,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,QAAQ,CAAC,kEAAkE,EAAE,GAAG,EAAE;IAChF,IAAI,MAAc,CAAC;IACnB,IAAI,QAAkB,CAAC;IAEvB,MAAM,SAAS,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,wBAAwB,CAAC,CAAC;IAEvF,UAAU,CAAC,GAAG,EAAE;QACd,MAAM,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,mBAAmB,CAAC,CAAC,CAAC;QACrE,QAAQ,GAAG,EAAE,CAAC;QACd,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;QACjD,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,kBAAkB,CAAC,CAAC,GAAG,IAAe,EAAE,EAAE;YACnE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,kBAAkB,CAAC,CAAC,GAAG,IAAe,EAAE,EAAE;YACjE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;QACH,kBAAkB,CAAC,SAAS,EAAE,CAAC;QAC/B,mBAAmB,CAAC,SAAS,EAAE,CAAC;IAClC,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE;QACb,EAAE,CAAC,eAAe,EAAE,CAAC;QACrB,0EAA0E;QAC1E,0EAA0E;QAC1E,6EAA6E;QAC7E,4EAA4E;QAC5E,2EAA2E;QAC3E,IAAI,CAAC;YACH,WAAW,CAAC,MAAM,CAAC,CAAC;QACtB,CAAC;QAAC,MAAM,CAAC;YACP,qDAAqD;QACvD,CAAC;IACH,CAAC,CAAC,CAAC;IAEH;;;;;;;OAOG;IACH,KAAK,UAAU,WAAW,CAAC,KAAe;QACxC,MAAM,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC;IAC7D,CAAC;IAED;;;;OAIG;IACH,KAAK,UAAU,cAAc,CAAC,IAAY,EAAE,YAAsB;QAChE,mBAAmB,CAAC,qBAAqB,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;QAClE,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;QACtD,IAAI,CAAC;YACH,MAAM,aAAa,CAAC,EAAyC,CAAC,CAAC;QACjE,CAAC;QAAC,MAAM,CAAC;YACP,mEAAmE;QACrE,CAAC;IACH,CAAC;IAED,EAAE,CAAC,2CAA2C,EAAE,KAAK,IAAI,EAAE;QACzD,MAAM,WAAW,CAAC,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC,CAAC;QAEjD,gEAAgE;QAChE,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/C,oEAAoE;QACpE,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;QAClD,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yEAAyE,EAAE,KAAK,IAAI,EAAE;QACvF,oEAAoE;QACpE,MAAM,WAAW,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC;QAEtC,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/C,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;QAClD,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qFAAqF,EAAE,KAAK,IAAI,EAAE;QACnG,2EAA2E;QAC3E,4EAA4E;QAC5E,8EAA8E;QAC9E,uEAAuE;QACvE,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,gBAAgB,CAAC,EACnC,8CAA8C,CAC/C,CAAC;QAEF,MAAM,WAAW,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC;QAE/C,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/C,4EAA4E;QAC5E,yEAAyE;QACzE,0EAA0E;QAC1E,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,4CAA4C,CAAC,CAAC;QAClF,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qEAAqE,EAAE,KAAK,IAAI,EAAE;QACnF,2EAA2E;QAC3E,0EAA0E;QAC1E,6EAA6E;QAC7E,MAAM,cAAc,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC,cAAc,EAAE,cAAc,CAAC,CAAC,CAAC;QAEhF,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/C,qEAAqE;QACrE,mEAAmE;QACnE,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,wCAAwC,CAAC,CAAC;QAC9E,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4EAA4E,EAAE,KAAK,IAAI,EAAE;QAC1F,6EAA6E;QAC7E,yEAAyE;QACzE,MAAM,WAAW,CAAC,CAAC,cAAc,EAAE,cAAc,CAAC,CAAC,CAAC;QAEpD,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;QACtD,0EAA0E;QAC1E,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -15,10 +15,32 @@ export declare function parseVerdict(content: string): {
|
|
|
15
15
|
pass: boolean;
|
|
16
16
|
reason: string;
|
|
17
17
|
} | null;
|
|
18
|
+
export type StructuredVerdictExtractionLayer = 'xml' | 'fence' | 'bare-json';
|
|
19
|
+
export type StructuredVerdictExtractionFailureCause = 'empty-output' | 'no-candidate' | 'invalid-json' | 'schema-invalid';
|
|
20
|
+
export interface StructuredVerdictExtractionAttempt {
|
|
21
|
+
layer: StructuredVerdictExtractionLayer;
|
|
22
|
+
cause: Extract<StructuredVerdictExtractionFailureCause, 'invalid-json' | 'schema-invalid'>;
|
|
23
|
+
/** Bounded validation summaries; candidate/output text is never retained here. */
|
|
24
|
+
issues?: string[];
|
|
25
|
+
}
|
|
26
|
+
export type StructuredVerdictExtractionResult = {
|
|
27
|
+
ok: true;
|
|
28
|
+
verdict: ShieldStructuredVerdict;
|
|
29
|
+
layer: StructuredVerdictExtractionLayer;
|
|
30
|
+
} | {
|
|
31
|
+
ok: false;
|
|
32
|
+
cause: StructuredVerdictExtractionFailureCause;
|
|
33
|
+
attempts: StructuredVerdictExtractionAttempt[];
|
|
34
|
+
};
|
|
18
35
|
/**
|
|
19
|
-
* Three-layer JSON extraction
|
|
20
|
-
*
|
|
21
|
-
*
|
|
36
|
+
* Three-layer JSON extraction with bounded, candidate-free failure diagnostics.
|
|
37
|
+
* A schema-invalid candidate takes precedence over malformed candidates because
|
|
38
|
+
* it is the most semantically advanced failure reached by the cascade.
|
|
39
|
+
*/
|
|
40
|
+
export declare function extractStructuredVerdictDetailed(content: string): StructuredVerdictExtractionResult;
|
|
41
|
+
/**
|
|
42
|
+
* Compatibility wrapper for callers that only need the parsed verdict.
|
|
43
|
+
* Returns null when all XML, fenced, and bare-JSON layers fail.
|
|
22
44
|
*/
|
|
23
45
|
export declare function extractStructuredVerdict(content: string): ShieldStructuredVerdict | null;
|
|
24
46
|
/**
|
|
@@ -74,10 +96,14 @@ export declare function writeReviewedContentHashValue(precomputedHash: string, c
|
|
|
74
96
|
* survives commits, amends, and rebases. Only breaks when source files change.
|
|
75
97
|
*
|
|
76
98
|
* Now a thin compose of the pure computer + explicit writer (Prop 304 R2): it
|
|
77
|
-
* hashes the CURRENT tree and stamps it. Retained at its original signature
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
99
|
+
* hashes the CURRENT tree and stamps it. Retained at its original signature for
|
|
100
|
+
* the no-changes stamp (an empty diff opens no mid-run LLM window) and
|
|
101
|
+
* `recordShieldOverride`, where there is no drift race to guard.
|
|
102
|
+
*
|
|
103
|
+
* NO LONGER used by the deterministic skip paths (all-non-code / filtered-empty /
|
|
104
|
+
* all-generated). Those drop the entire diff without examining it, so they are
|
|
105
|
+
* non-reviews and must not stamp — see `NO_STAMP_NOTICE` (mmnto-ai/totem#2466).
|
|
106
|
+
* The LLM review path does NOT use this — it
|
|
81
107
|
* captures the hash pre-fan and compare-and-stamps in `shieldCommand` /
|
|
82
108
|
* `handleVerdictResult` so a mid-review edit can never be authorized.
|
|
83
109
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shield.d.ts","sourceRoot":"","sources":["../../src/commands/shield.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA2B,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEvF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAkBzE,OAAO,EAWL,KAAK,aAAa,EAClB,KAAK,uBAAuB,EAO7B,MAAM,uBAAuB,CAAC;AAK/B,OAAO,EACL,cAAc,EACd,0BAA0B,EAC1B,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAI/B,UAAU,gBAAgB;IACxB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB,OAAO,EAAE,YAAY,EAAE,CAAC;CACzB;AA0BD,wBAAsB,gBAAgB,CACpC,YAAY,EAAE,MAAM,EAAE,EACtB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CA0CjB;AAID,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EAAE,EACtB,OAAO,EAAE,gBAAgB,EACzB,YAAY,EAAE,MAAM,EACpB,UAAU,CAAC,EAAE,MAAM,EAAE,EACrB,WAAW,CAAC,EAAE,MAAM,EACpB,wBAAwB,CAAC,EAAE,MAAM,GAChC,MAAM,CA4DR;AAID,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EAAE,EACtB,YAAY,EAAE,MAAM,EACpB,UAAU,CAAC,EAAE,MAAM,EAAE,EACrB,WAAW,CAAC,EAAE,MAAM,EACpB,wBAAwB,CAAC,EAAE,MAAM,GAChC,MAAM,CA0CR;AAID,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAItF;AAID;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,uBAAuB,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"shield.d.ts","sourceRoot":"","sources":["../../src/commands/shield.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/shield.js
CHANGED
|
@@ -152,54 +152,84 @@ export function parseVerdict(content) {
|
|
|
152
152
|
return null;
|
|
153
153
|
return { pass: match[1] === 'PASS', reason: match[2].trim() };
|
|
154
154
|
}
|
|
155
|
-
|
|
155
|
+
const MAX_EXTRACTION_ISSUES = 4;
|
|
156
|
+
const MAX_EXTRACTION_ISSUE_CHARS = 160;
|
|
157
|
+
function summarizeValidationIssues(issues) {
|
|
158
|
+
return issues.slice(0, MAX_EXTRACTION_ISSUES).map((issue) => {
|
|
159
|
+
const path = issue.path.length > 0 ? issue.path.join('.') : '<root>';
|
|
160
|
+
return `${path}: ${issue.code}`.slice(0, MAX_EXTRACTION_ISSUE_CHARS);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function tryStructuredVerdictCandidate(layer, candidate) {
|
|
164
|
+
let parsed;
|
|
165
|
+
try {
|
|
166
|
+
parsed = JSON.parse(candidate);
|
|
167
|
+
// totem-context: malformed candidates are expected during the extraction cascade and are converted into the typed invalid-json result below.
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return { ok: false, attempt: { layer, cause: 'invalid-json' } };
|
|
171
|
+
}
|
|
172
|
+
const result = ShieldStructuredVerdictSchema.safeParse(parsed);
|
|
173
|
+
if (result.success)
|
|
174
|
+
return { ok: true, verdict: result.data, layer };
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
attempt: {
|
|
178
|
+
layer,
|
|
179
|
+
cause: 'schema-invalid',
|
|
180
|
+
issues: summarizeValidationIssues(result.error.issues),
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
156
184
|
/**
|
|
157
|
-
* Three-layer JSON extraction
|
|
158
|
-
*
|
|
159
|
-
*
|
|
185
|
+
* Three-layer JSON extraction with bounded, candidate-free failure diagnostics.
|
|
186
|
+
* A schema-invalid candidate takes precedence over malformed candidates because
|
|
187
|
+
* it is the most semantically advanced failure reached by the cascade.
|
|
160
188
|
*/
|
|
161
|
-
export function
|
|
162
|
-
|
|
189
|
+
export function extractStructuredVerdictDetailed(content) {
|
|
190
|
+
if (content.trim().length === 0)
|
|
191
|
+
return { ok: false, cause: 'empty-output', attempts: [] };
|
|
192
|
+
const attempts = [];
|
|
193
|
+
const tryCandidate = (layer, candidate) => {
|
|
194
|
+
const result = tryStructuredVerdictCandidate(layer, candidate);
|
|
195
|
+
if (result.ok)
|
|
196
|
+
return result.verdict;
|
|
197
|
+
attempts.push(result.attempt);
|
|
198
|
+
return null;
|
|
199
|
+
};
|
|
163
200
|
const xmlMatch = content.match(/<shield_verdict>([\s\S]*?)<\/shield_verdict>/);
|
|
164
201
|
if (xmlMatch) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (result.success)
|
|
169
|
-
return result.data;
|
|
170
|
-
}
|
|
171
|
-
catch {
|
|
172
|
-
// Move to next layer
|
|
173
|
-
}
|
|
202
|
+
const verdict = tryCandidate('xml', xmlMatch[1]);
|
|
203
|
+
if (verdict)
|
|
204
|
+
return { ok: true, verdict, layer: 'xml' };
|
|
174
205
|
}
|
|
175
|
-
// Layer 2 — Markdown code fences (fallback)
|
|
176
206
|
const fenceMatch = content.match(/(?:```|~~~)(?:json)?\s*\n([\s\S]*?)\n\s*(?:```|~~~)/);
|
|
177
207
|
if (fenceMatch) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (result.success)
|
|
182
|
-
return result.data;
|
|
183
|
-
}
|
|
184
|
-
catch {
|
|
185
|
-
// Move to next layer
|
|
186
|
-
}
|
|
208
|
+
const verdict = tryCandidate('fence', fenceMatch[1]);
|
|
209
|
+
if (verdict)
|
|
210
|
+
return { ok: true, verdict, layer: 'fence' };
|
|
187
211
|
}
|
|
188
|
-
// Layer 3 — Bare JSON (last resort, guarded by "findings" keyword)
|
|
189
212
|
const firstBrace = content.indexOf('{');
|
|
190
213
|
const lastBrace = content.lastIndexOf('}');
|
|
191
214
|
if (firstBrace !== -1 && lastBrace > firstBrace && content.includes('"findings"')) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
|
|
215
|
+
const verdict = tryCandidate('bare-json', content.slice(firstBrace, lastBrace + 1));
|
|
216
|
+
if (verdict)
|
|
217
|
+
return { ok: true, verdict, layer: 'bare-json' };
|
|
218
|
+
}
|
|
219
|
+
if (attempts.length === 0)
|
|
220
|
+
return { ok: false, cause: 'no-candidate', attempts };
|
|
221
|
+
const cause = attempts.some((attempt) => attempt.cause === 'schema-invalid')
|
|
222
|
+
? 'schema-invalid'
|
|
223
|
+
: 'invalid-json';
|
|
224
|
+
return { ok: false, cause, attempts };
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Compatibility wrapper for callers that only need the parsed verdict.
|
|
228
|
+
* Returns null when all XML, fenced, and bare-JSON layers fail.
|
|
229
|
+
*/
|
|
230
|
+
export function extractStructuredVerdict(content) {
|
|
231
|
+
const result = extractStructuredVerdictDetailed(content);
|
|
232
|
+
return result.ok ? result.verdict : null;
|
|
203
233
|
}
|
|
204
234
|
/**
|
|
205
235
|
* Deterministic pass/fail based on findings.
|
|
@@ -268,6 +298,17 @@ export function formatVerdictForDisplay(verdict, pass) {
|
|
|
268
298
|
* (callers on the pre-#1527 signature) so behavior is preserved.
|
|
269
299
|
*/
|
|
270
300
|
const LEGACY_REVIEW_SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
|
|
301
|
+
/**
|
|
302
|
+
* Shared tail for every deterministic skip that drops the ENTIRE diff
|
|
303
|
+
* (mmnto-ai/totem#2466). Such a run is a NON-REVIEW, not a pass: no lane ran and
|
|
304
|
+
* nothing was examined, so it must not stamp `.reviewed-content-hash` and must not
|
|
305
|
+
* read as a clean review.
|
|
306
|
+
*
|
|
307
|
+
* Single-sourced deliberately — the three skip sites must never drift into
|
|
308
|
+
* describing the same guarantee three different ways. Voice matches the existing
|
|
309
|
+
* worktree-drift notices, which are the other "NOT stamped" surface.
|
|
310
|
+
*/
|
|
311
|
+
const NO_STAMP_NOTICE = 'Nothing was examined, so the reviewed-content-hash was NOT stamped — this run does not authorize a push.';
|
|
271
312
|
/**
|
|
272
313
|
* Refresh `<totemDir>/review-extensions.txt` if its contents do not match
|
|
273
314
|
* the supplied extension set (or the file is missing). Closes the stale-
|
|
@@ -396,10 +437,14 @@ export async function writeReviewedContentHashValue(precomputedHash, cwd, totemD
|
|
|
396
437
|
* survives commits, amends, and rebases. Only breaks when source files change.
|
|
397
438
|
*
|
|
398
439
|
* Now a thin compose of the pure computer + explicit writer (Prop 304 R2): it
|
|
399
|
-
* hashes the CURRENT tree and stamps it. Retained at its original signature
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
440
|
+
* hashes the CURRENT tree and stamps it. Retained at its original signature for
|
|
441
|
+
* the no-changes stamp (an empty diff opens no mid-run LLM window) and
|
|
442
|
+
* `recordShieldOverride`, where there is no drift race to guard.
|
|
443
|
+
*
|
|
444
|
+
* NO LONGER used by the deterministic skip paths (all-non-code / filtered-empty /
|
|
445
|
+
* all-generated). Those drop the entire diff without examining it, so they are
|
|
446
|
+
* non-reviews and must not stamp — see `NO_STAMP_NOTICE` (mmnto-ai/totem#2466).
|
|
447
|
+
* The LLM review path does NOT use this — it
|
|
403
448
|
* captures the hash pre-fan and compare-and-stamps in `shieldCommand` /
|
|
404
449
|
* `handleVerdictResult` so a mid-review edit can never be authorized.
|
|
405
450
|
*/
|
|
@@ -1083,11 +1128,14 @@ export async function shieldCommand(options) {
|
|
|
1083
1128
|
changedFiles = generated.keptFiles;
|
|
1084
1129
|
generatedArtifactSummary = buildGeneratedArtifactSection(generated.summaries);
|
|
1085
1130
|
// All changed files were generated artifacts — nothing left to review.
|
|
1086
|
-
//
|
|
1087
|
-
//
|
|
1131
|
+
// Not sending them to the LLM stays correct (their correctness is a gate
|
|
1132
|
+
// concern, not the LLM's) but that does NOT extend to stamping the push
|
|
1133
|
+
// gate on their behalf: this is the one skip path that can drop a TRACKED,
|
|
1134
|
+
// HASHED source file, because `.gitattributes linguist-generated` can mark
|
|
1135
|
+
// a `.ts` as generated. Stamping here would authorize code no reviewer saw
|
|
1136
|
+
// (mmnto-ai/totem#2466).
|
|
1088
1137
|
if (!diff.trim()) {
|
|
1089
|
-
log.
|
|
1090
|
-
await writeReviewedContentHash(cwd, config.totemDir, configRoot, config.review.sourceExtensions);
|
|
1138
|
+
log.warn(DISPLAY_TAG, `NON-REVIEW: every changed file is a generated artifact, so no lane ran. ${NO_STAMP_NOTICE}`);
|
|
1091
1139
|
return;
|
|
1092
1140
|
}
|
|
1093
1141
|
}
|
|
@@ -1095,9 +1143,8 @@ export async function shieldCommand(options) {
|
|
|
1095
1143
|
// Stage 1: Classify files — fast-path for non-code-only diffs
|
|
1096
1144
|
const classification = classifyChangedFiles(changedFiles);
|
|
1097
1145
|
if (classification.allNonCode) {
|
|
1098
|
-
log.
|
|
1146
|
+
log.warn(DISPLAY_TAG, `NON-REVIEW: every changed file is non-code, so no lane ran. ${NO_STAMP_NOTICE}`);
|
|
1099
1147
|
log.dim(DISPLAY_TAG, `Skipped: ${changedFiles.join(', ')}`);
|
|
1100
|
-
await writeReviewedContentHash(cwd, config.totemDir, configRoot, config.review.sourceExtensions);
|
|
1101
1148
|
return;
|
|
1102
1149
|
}
|
|
1103
1150
|
// Stage 2: Filter diff to code-only files for mixed diffs
|
|
@@ -1107,9 +1154,8 @@ export async function shieldCommand(options) {
|
|
|
1107
1154
|
filteredDiff = await filterDiffByPatterns(diff, classification.nonCodeFiles);
|
|
1108
1155
|
filteredFiles = classification.codeFiles;
|
|
1109
1156
|
if (!filteredDiff.trim()) {
|
|
1110
|
-
// After filtering, no code diff remains —
|
|
1111
|
-
log.
|
|
1112
|
-
await writeReviewedContentHash(cwd, config.totemDir, configRoot, config.review.sourceExtensions);
|
|
1157
|
+
// After filtering non-code files, no code diff remains — nothing was examined.
|
|
1158
|
+
log.warn(DISPLAY_TAG, `NON-REVIEW: no code changes remain after filtering non-code files, so no lane ran. ${NO_STAMP_NOTICE}`);
|
|
1113
1159
|
return;
|
|
1114
1160
|
}
|
|
1115
1161
|
log.dim(DISPLAY_TAG, `Filtered ${classification.nonCodeFiles.length} non-code file(s) from diff`);
|