@warpgogol/forge 5.2.0 → 5.2.4
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/AGENTS.md +4 -1
- package/dist/os/adr/adr.module.js +1 -1
- package/dist/os/adr/adr.module.js.map +1 -1
- package/dist/os/compass/compass.module.js +2 -2
- package/dist/os/compass/compass.module.js.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.d.ts +3 -0
- package/dist/os/compass/handlers/compass-audit-handler.d.ts.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.js +87 -8
- package/dist/os/compass/handlers/compass-audit-handler.js.map +1 -1
- package/dist/os/core/core.module.d.ts.map +1 -1
- package/dist/os/core/core.module.js +15 -1
- package/dist/os/core/core.module.js.map +1 -1
- package/dist/os/mission/handlers/archive.d.ts.map +1 -1
- package/dist/os/mission/handlers/archive.js +22 -6
- package/dist/os/mission/handlers/archive.js.map +1 -1
- package/dist/os/program/lease.js +1 -1
- package/dist/os/program/lease.js.map +1 -1
- package/dist/os/queue/queue.module.js +1 -1
- package/dist/os/queue/queue.module.js.map +1 -1
- package/dist/os/rfc/handlers/list-create.d.ts.map +1 -1
- package/dist/os/rfc/handlers/list-create.js +79 -32
- package/dist/os/rfc/handlers/list-create.js.map +1 -1
- package/dist/os/rfc/rfc.module.js +1 -1
- package/dist/os/rfc/rfc.module.js.map +1 -1
- package/dist/os/spec/spec.module.d.ts.map +1 -1
- package/dist/os/spec/spec.module.js +2 -0
- package/dist/os/spec/spec.module.js.map +1 -1
- package/dist/src/config/forge-config.d.ts +9 -0
- package/dist/src/config/forge-config.d.ts.map +1 -1
- package/dist/src/config/forge-config.js +7 -0
- package/dist/src/config/forge-config.js.map +1 -1
- package/dist/src/knowledge/index.d.ts +1 -0
- package/dist/src/knowledge/index.d.ts.map +1 -1
- package/dist/src/knowledge/index.js +1 -0
- package/dist/src/knowledge/index.js.map +1 -1
- package/dist/src/knowledge/sync.d.ts +24 -0
- package/dist/src/knowledge/sync.d.ts.map +1 -0
- package/dist/src/knowledge/sync.js +127 -0
- package/dist/src/knowledge/sync.js.map +1 -0
- package/dist/src/onboarding/agents-generate.d.ts +2 -0
- package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
- package/dist/src/onboarding/agents-generate.js +14 -18
- package/dist/src/onboarding/agents-generate.js.map +1 -1
- package/dist/src/onboarding/doctor.d.ts +6 -0
- package/dist/src/onboarding/doctor.d.ts.map +1 -1
- package/dist/src/onboarding/doctor.js +124 -14
- package/dist/src/onboarding/doctor.js.map +1 -1
- package/dist/src/onboarding/init.d.ts.map +1 -1
- package/dist/src/onboarding/init.js +15 -8
- package/dist/src/onboarding/init.js.map +1 -1
- package/dist/src/onboarding/memory-compact.d.ts +22 -0
- package/dist/src/onboarding/memory-compact.d.ts.map +1 -0
- package/dist/src/onboarding/memory-compact.js +147 -0
- package/dist/src/onboarding/memory-compact.js.map +1 -0
- package/dist/src/onboarding/memory-scaffold.d.ts +1 -0
- package/dist/src/onboarding/memory-scaffold.d.ts.map +1 -1
- package/dist/src/onboarding/memory-scaffold.js +1 -1
- package/dist/src/onboarding/memory-scaffold.js.map +1 -1
- package/dist/src/onboarding/nested-agents-generate.d.ts.map +1 -1
- package/dist/src/onboarding/nested-agents-generate.js +1 -1
- package/dist/src/onboarding/nested-agents-generate.js.map +1 -1
- package/dist/src/onboarding/upgrade.d.ts.map +1 -1
- package/dist/src/onboarding/upgrade.js +42 -13
- package/dist/src/onboarding/upgrade.js.map +1 -1
- package/dist/src/onboarding/workspace-discovery.d.ts +1 -1
- package/dist/src/onboarding/workspace-discovery.d.ts.map +1 -1
- package/dist/src/onboarding/workspace-discovery.js +14 -2
- package/dist/src/onboarding/workspace-discovery.js.map +1 -1
- package/dist/src/types.d.ts +1 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +1 -0
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils/fs-atomic.js +1 -1
- package/dist/src/utils/fs-atomic.js.map +1 -1
- package/docs/reference/forge-yaml.md +12 -0
- package/os/adr/adr.module.ts +1 -1
- package/os/compass/compass.module.ts +2 -2
- package/os/compass/handlers/compass-audit-handler.ts +116 -8
- package/os/compass/handlers/tests/compass-audit-plan.test.ts +98 -0
- package/os/compass/handlers/tests/compass-audit-record.test.ts +105 -0
- package/os/compass/handlers/tests/compass-audit-validate.test.ts +133 -0
- package/os/compass/handlers/tests/compass-ledger-scope.test.ts +124 -0
- package/os/core/core.module.ts +16 -1
- package/os/mission/handlers/archive.test.ts +49 -0
- package/os/mission/handlers/archive.ts +21 -6
- package/os/program/lease.ts +1 -1
- package/os/queue/queue.module.ts +1 -1
- package/os/rfc/handlers/list-create.ts +83 -35
- package/os/rfc/handlers/validate-rules.test.ts +50 -0
- package/os/rfc/rfc-create-concurrent.test.ts +118 -0
- package/os/rfc/rfc-create-hint.test.ts +90 -0
- package/os/rfc/rfc-read-only-no-side-effects.test.ts +83 -0
- package/os/rfc/rfc.module.ts +1 -1
- package/os/spec/spec.module.ts +2 -0
- package/package.json +1 -1
- package/profiles/forge-shell.yaml +3 -0
- package/profiles/godot-game.yaml +3 -0
- package/profiles/phaser-game.yaml +3 -0
- package/profiles/site-workshop.yaml +567 -564
- package/src/config/forge-config.ts +11 -0
- package/src/knowledge/__tests__/sync.test.ts +227 -0
- package/src/knowledge/index.ts +7 -0
- package/src/knowledge/sync.ts +161 -0
- package/src/onboarding/agents-generate.ts +18 -18
- package/src/onboarding/doctor.ts +141 -15
- package/src/onboarding/init.ts +15 -8
- package/src/onboarding/memory-compact.ts +188 -0
- package/src/onboarding/memory-scaffold.ts +1 -1
- package/src/onboarding/nested-agents-generate.ts +5 -1
- package/src/onboarding/upgrade.ts +47 -16
- package/src/onboarding/workspace-discovery.ts +15 -1
- package/src/tests/agents-generate.test.ts +26 -3
- package/src/tests/doctor-dist-freshness.test.ts +110 -0
- package/src/tests/doctor-fix.test.ts +145 -0
- package/src/tests/doctor-nested-agents.test.ts +108 -0
- package/src/tests/memory-compact.test.ts +143 -0
- package/src/tests/upgrade.test.ts +53 -0
- package/src/tests/workspace-discovery.test.ts +28 -0
- package/src/types.ts +2 -2
- package/src/utils/fs-atomic.ts +1 -1
|
@@ -13,12 +13,14 @@ mode (RFC-0556). Drives per-file semantic-truth auditing on a revision cadence (
|
|
|
13
13
|
<item>RFC-0352: initial implementation of compass.audit.plan, compass.audit.record, compass.audit.baseline, compass.audit.validate.</item>
|
|
14
14
|
<item>RFC-0556: moved from @warpgogol/site-kernel-checks to @warpgogol/forge for autonomous mode.</item>
|
|
15
15
|
<item>RFC-1094: audit work orders now carry the KEY_DECISIONS block alongside MODULE_CONTRACT and CHANGE_SUMMARY.</item>
|
|
16
|
+
<item>RFC-1139: CLI hint accuracy and agent-safety hygiene — rfc.create hint, EC-14-PARTIAL, amend delegation, ledger scope, sync footer, mission.open remnants</item>
|
|
17
|
+
<item>RFC-1143: validate/plan/record apply filterLedgerEligiblePaths — ledger-ineligible authored paths (missions/, gitignored) are skipped with skippedIneligible diagnostics instead of failing COMPASS-AUDIT-01; record warns but still writes.</item>
|
|
16
18
|
</CHANGE_SUMMARY>
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
|
-
import { readFile } from "node:fs/promises";
|
|
21
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
20
22
|
import { resolve, relative } from "node:path";
|
|
21
|
-
import { execFile } from "node:child_process";
|
|
23
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
22
24
|
import { promisify } from "node:util";
|
|
23
25
|
import { createCompassInventoryEntries } from "./compass-inventory.ts";
|
|
24
26
|
import { resolveCompassPolicy } from "../policy.ts";
|
|
@@ -37,6 +39,9 @@ import { parse as yamlParse, stringify as yamlStringify } from "yaml";
|
|
|
37
39
|
const execFileAsync = promisify(execFile);
|
|
38
40
|
const LEDGER_PATH = "docs/compass-audit-ledger.generated.yaml";
|
|
39
41
|
const DEFAULT_THRESHOLD = 30;
|
|
42
|
+
// RFC-1143: cap the skippedPaths diagnostic list so huge ineligible sets do not
|
|
43
|
+
// flood the output; the pretty summary appends "and N more" when truncated.
|
|
44
|
+
const SKIPPED_PATHS_CAP = 20;
|
|
40
45
|
|
|
41
46
|
function resolveRevisionThreshold(value: unknown, fallback: number): number {
|
|
42
47
|
if (value === undefined) return fallback;
|
|
@@ -99,6 +104,7 @@ async function saveLedger(workspaceRoot: string, ledger: CompassAuditLedger): Pr
|
|
|
99
104
|
const normalized = withLedgerAdvisory(ledger);
|
|
100
105
|
normalized.entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
101
106
|
const abs = resolve(workspaceRoot, LEDGER_PATH);
|
|
107
|
+
await mkdir(resolve(abs, ".."), { recursive: true });
|
|
102
108
|
const header = buildGeneratedHeader({
|
|
103
109
|
ownerCommand: "compass.audit.record",
|
|
104
110
|
filePath: LEDGER_PATH,
|
|
@@ -129,6 +135,54 @@ function getAuthoredEntries(entries: CompassInventoryEntry[]): CompassInventoryE
|
|
|
129
135
|
);
|
|
130
136
|
}
|
|
131
137
|
|
|
138
|
+
// RFC-1139: the tracked ledger must not accumulate entries for paths that
|
|
139
|
+
// can never be audited long-term — gitignored roots (missions/, dist/) and
|
|
140
|
+
// the missions/ tree specifically (workpieces move to archive on close).
|
|
141
|
+
// Batch-checks `git check-ignore --stdin` once for all candidate paths.
|
|
142
|
+
async function filterLedgerEligiblePaths(
|
|
143
|
+
workspaceRoot: string,
|
|
144
|
+
paths: string[],
|
|
145
|
+
): Promise<Set<string>> {
|
|
146
|
+
const eligible = new Set<string>();
|
|
147
|
+
const candidates = paths.filter((p) => !p.startsWith("missions/"));
|
|
148
|
+
let ignored = new Set<string>();
|
|
149
|
+
if (candidates.length > 0) {
|
|
150
|
+
try {
|
|
151
|
+
const stdout = execFileSync("git", ["check-ignore", "--stdin"], {
|
|
152
|
+
cwd: workspaceRoot,
|
|
153
|
+
input: candidates.join("\n"),
|
|
154
|
+
encoding: "utf-8",
|
|
155
|
+
});
|
|
156
|
+
ignored = new Set(stdout.split("\n").filter((l: string) => l.length > 0));
|
|
157
|
+
} catch {
|
|
158
|
+
// check-ignore exits 1 when nothing is ignored — treat all as eligible
|
|
159
|
+
ignored = new Set();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
for (const p of candidates) {
|
|
163
|
+
if (!ignored.has(p)) eligible.add(p);
|
|
164
|
+
}
|
|
165
|
+
return eligible;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// RFC-1143: partition authored entries into ledger-eligible and ineligible
|
|
169
|
+
// sets. Shared by plan/validate so the predicate lives in exactly one place.
|
|
170
|
+
async function partitionLedgerEligibleAuthored(
|
|
171
|
+
workspaceRoot: string,
|
|
172
|
+
authored: CompassInventoryEntry[],
|
|
173
|
+
): Promise<{ eligible: CompassInventoryEntry[]; ineligible: CompassInventoryEntry[] }> {
|
|
174
|
+
const eligiblePaths = await filterLedgerEligiblePaths(
|
|
175
|
+
workspaceRoot,
|
|
176
|
+
authored.map((e) => e.path),
|
|
177
|
+
);
|
|
178
|
+
const eligible: CompassInventoryEntry[] = [];
|
|
179
|
+
const ineligible: CompassInventoryEntry[] = [];
|
|
180
|
+
for (const entry of authored) {
|
|
181
|
+
(eligiblePaths.has(entry.path) ? eligible : ineligible).push(entry);
|
|
182
|
+
}
|
|
183
|
+
return { eligible, ineligible };
|
|
184
|
+
}
|
|
185
|
+
|
|
132
186
|
export async function runCompassAuditPlan(
|
|
133
187
|
input: ForgeCommandInput,
|
|
134
188
|
context: ForgeRuntimeContext,
|
|
@@ -137,6 +191,7 @@ export async function runCompassAuditPlan(
|
|
|
137
191
|
threshold: number;
|
|
138
192
|
dueCount: number;
|
|
139
193
|
items: CompassAuditWorkOrderItem[];
|
|
194
|
+
skippedIneligible: number;
|
|
140
195
|
}>
|
|
141
196
|
> {
|
|
142
197
|
const scanRoot = resolveCompassScanRoot(input, context);
|
|
@@ -153,6 +208,15 @@ export async function runCompassAuditPlan(
|
|
|
153
208
|
|
|
154
209
|
const threshold = resolveRevisionThreshold(input.flags["threshold"], ledger.revisionThreshold);
|
|
155
210
|
|
|
211
|
+
// RFC-1143: drop ledger-ineligible authored paths (missions/, gitignored)
|
|
212
|
+
// before the per-entry revision loop — they can never be audited long-term
|
|
213
|
+
// and must not cost a git call each.
|
|
214
|
+
const { eligible: eligibleAuthored, ineligible } = await partitionLedgerEligibleAuthored(
|
|
215
|
+
context.workspaceRoot,
|
|
216
|
+
authored,
|
|
217
|
+
);
|
|
218
|
+
const skippedIneligible = ineligible.length;
|
|
219
|
+
|
|
156
220
|
const ledgerMap = new Map<string, CompassAuditLedgerEntry>();
|
|
157
221
|
for (const e of ledger.entries) {
|
|
158
222
|
ledgerMap.set(e.path, e);
|
|
@@ -160,7 +224,7 @@ export async function runCompassAuditPlan(
|
|
|
160
224
|
|
|
161
225
|
const items: CompassAuditWorkOrderItem[] = [];
|
|
162
226
|
|
|
163
|
-
for (const entry of
|
|
227
|
+
for (const entry of eligibleAuthored) {
|
|
164
228
|
const ledgerEntry = ledgerMap.get(entry.path);
|
|
165
229
|
const auditedRevision = ledgerEntry?.auditedRevision ?? null;
|
|
166
230
|
|
|
@@ -192,10 +256,12 @@ export async function runCompassAuditPlan(
|
|
|
192
256
|
|
|
193
257
|
items.sort((a, b) => a.path.localeCompare(b.path));
|
|
194
258
|
|
|
195
|
-
context.logger.info(
|
|
259
|
+
context.logger.info(
|
|
260
|
+
`[compass.audit.plan] threshold=${threshold}, due=${items.length}, skippedIneligible=${skippedIneligible}`,
|
|
261
|
+
);
|
|
196
262
|
|
|
197
263
|
return {
|
|
198
|
-
data: { threshold, dueCount: items.length, items },
|
|
264
|
+
data: { threshold, dueCount: items.length, items, skippedIneligible },
|
|
199
265
|
exitCode: 0,
|
|
200
266
|
summary: `[compass.audit.plan] ${items.length} files due for audit (threshold=${threshold})`,
|
|
201
267
|
nextSteps:
|
|
@@ -235,6 +301,16 @@ export async function runCompassAuditRecord(
|
|
|
235
301
|
|
|
236
302
|
const filePath = relative(context.workspaceRoot, resolve(process.cwd(), rawFilePath));
|
|
237
303
|
|
|
304
|
+
// RFC-1143: warn when recording an audit for a ledger-ineligible path — the
|
|
305
|
+
// entry will be swept by the next baseline run (RFC-1139). Operator intent
|
|
306
|
+
// wins: the write still proceeds.
|
|
307
|
+
const recordEligible = await filterLedgerEligiblePaths(context.workspaceRoot, [filePath]);
|
|
308
|
+
if (!recordEligible.has(filePath)) {
|
|
309
|
+
context.logger.warn(
|
|
310
|
+
`[compass.audit.record] ${filePath}: path is ledger-ineligible (missions/ or gitignored) — this entry will be swept by the next compass.audit.baseline run`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
238
314
|
const { revision, entityId, contentHash } = await getRevisionByPath(
|
|
239
315
|
context.workspaceRoot,
|
|
240
316
|
filePath,
|
|
@@ -302,11 +378,17 @@ export async function runCompassAuditBaseline(
|
|
|
302
378
|
const authored = getAuthoredEntries(entries);
|
|
303
379
|
const ledger = await loadLedger(context.workspaceRoot);
|
|
304
380
|
|
|
381
|
+
// RFC-1139: drop ineligible entries (missions/, gitignored) already in the
|
|
382
|
+
// ledger — self-healing cleanup — and never seed new ones.
|
|
383
|
+
const candidatePaths = [...authored.map((e) => e.path), ...ledger.entries.map((e) => e.path)];
|
|
384
|
+
const eligible = await filterLedgerEligiblePaths(context.workspaceRoot, candidatePaths);
|
|
385
|
+
ledger.entries = ledger.entries.filter((e) => eligible.has(e.path));
|
|
386
|
+
|
|
305
387
|
const existingPaths = new Set(ledger.entries.map((e) => e.path));
|
|
306
388
|
let seededCount = 0;
|
|
307
389
|
|
|
308
390
|
for (const entry of authored) {
|
|
309
|
-
if (existingPaths.has(entry.path)) {
|
|
391
|
+
if (!eligible.has(entry.path) || existingPaths.has(entry.path)) {
|
|
310
392
|
continue;
|
|
311
393
|
}
|
|
312
394
|
|
|
@@ -358,6 +440,8 @@ export async function runCompassAuditValidate(
|
|
|
358
440
|
message: string;
|
|
359
441
|
fix: string;
|
|
360
442
|
}>;
|
|
443
|
+
skippedIneligible: number;
|
|
444
|
+
skippedPaths: string[];
|
|
361
445
|
}>
|
|
362
446
|
> {
|
|
363
447
|
const strict = input.flags["strict"] === true;
|
|
@@ -373,6 +457,16 @@ export async function runCompassAuditValidate(
|
|
|
373
457
|
const authored = getAuthoredEntries(entries);
|
|
374
458
|
const ledger = await loadLedger(context.workspaceRoot);
|
|
375
459
|
|
|
460
|
+
// RFC-1143: drop ledger-ineligible authored paths (missions/, gitignored)
|
|
461
|
+
// before the per-entry revision loop — baseline is forbidden to seed them,
|
|
462
|
+
// so demanding entries is a guaranteed false-positive (COMPASS-AUDIT-01).
|
|
463
|
+
const { eligible: eligibleAuthored, ineligible } = await partitionLedgerEligibleAuthored(
|
|
464
|
+
context.workspaceRoot,
|
|
465
|
+
authored,
|
|
466
|
+
);
|
|
467
|
+
const skippedPaths = ineligible.map((e) => e.path).sort();
|
|
468
|
+
const skippedIneligible = skippedPaths.length;
|
|
469
|
+
|
|
376
470
|
const ledgerMap = new Map<string, CompassAuditLedgerEntry>();
|
|
377
471
|
for (const e of ledger.entries) {
|
|
378
472
|
ledgerMap.set(e.path, e);
|
|
@@ -386,7 +480,7 @@ export async function runCompassAuditValidate(
|
|
|
386
480
|
fix: string;
|
|
387
481
|
}> = [];
|
|
388
482
|
|
|
389
|
-
for (const entry of
|
|
483
|
+
for (const entry of eligibleAuthored) {
|
|
390
484
|
const ledgerEntry = ledgerMap.get(entry.path);
|
|
391
485
|
const auditedRevision = ledgerEntry?.auditedRevision ?? null;
|
|
392
486
|
|
|
@@ -415,8 +509,22 @@ export async function runCompassAuditValidate(
|
|
|
415
509
|
const dueCount = diagnostics.length;
|
|
416
510
|
const hasErrors = strict && dueCount > 0;
|
|
417
511
|
|
|
512
|
+
if (skippedIneligible > 0) {
|
|
513
|
+
const shown = skippedPaths.slice(0, SKIPPED_PATHS_CAP);
|
|
514
|
+
const more = skippedIneligible - shown.length;
|
|
515
|
+
context.logger.info(
|
|
516
|
+
`[compass.audit.validate] skipped ${skippedIneligible} ledger-ineligible path(s) (missions/, gitignored)${more > 0 ? ` and ${more} more` : ""}`,
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
418
520
|
return {
|
|
419
|
-
data: {
|
|
521
|
+
data: {
|
|
522
|
+
strict,
|
|
523
|
+
dueCount,
|
|
524
|
+
diagnostics,
|
|
525
|
+
skippedIneligible,
|
|
526
|
+
skippedPaths: skippedPaths.slice(0, SKIPPED_PATHS_CAP),
|
|
527
|
+
},
|
|
420
528
|
exitCode: hasErrors ? 1 : 0,
|
|
421
529
|
summary: dueCount > 0 ? undefined : `[compass.audit.validate] OK (0 files overdue)`,
|
|
422
530
|
nextSteps:
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>
|
|
4
|
+
RFC-1143 AC-4: compass.audit.plan must not list ledger-ineligible paths
|
|
5
|
+
(missions/, gitignored) in its due work-order items and must report the
|
|
6
|
+
skippedIneligible count.
|
|
7
|
+
</purpose>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
12
|
+
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { execFile } from "node:child_process";
|
|
16
|
+
import { promisify } from "node:util";
|
|
17
|
+
import { runCompassAuditPlan } from "../compass-audit-handler.ts";
|
|
18
|
+
import type { ForgeCommandInput, ForgeRuntimeContext } from "../../../../src/types.ts";
|
|
19
|
+
|
|
20
|
+
const execFileAsync = promisify(execFile);
|
|
21
|
+
|
|
22
|
+
const logger = {
|
|
23
|
+
section() {},
|
|
24
|
+
info() {},
|
|
25
|
+
warn() {},
|
|
26
|
+
error() {},
|
|
27
|
+
success() {},
|
|
28
|
+
getEvents() {
|
|
29
|
+
return [];
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function makeContext(workspaceRoot: string): ForgeRuntimeContext {
|
|
34
|
+
return {
|
|
35
|
+
workspaceRoot,
|
|
36
|
+
site: undefined,
|
|
37
|
+
siteExplicit: false,
|
|
38
|
+
logger: logger as never,
|
|
39
|
+
dryRun: false,
|
|
40
|
+
outputFormat: "json",
|
|
41
|
+
io: {} as never,
|
|
42
|
+
actualState: undefined as never,
|
|
43
|
+
fileIntents: [],
|
|
44
|
+
} as unknown as ForgeRuntimeContext;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// missions/ is not in the default scanRoots — pass both roots so the fixture
|
|
48
|
+
// produces a mixed eligible/ineligible authored set.
|
|
49
|
+
const SCAN_ROOTS = ["missions", "packages"];
|
|
50
|
+
|
|
51
|
+
function makeInput(flags: Record<string, unknown> = {}): ForgeCommandInput {
|
|
52
|
+
return { argv: [], flags: { root: SCAN_ROOTS, ...flags } } as unknown as ForgeCommandInput;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const ELIGIBLE_FILE = "packages/foo/index.ts";
|
|
56
|
+
|
|
57
|
+
describe("compass.audit.plan — RFC-1143 ledger-ineligible skips", () => {
|
|
58
|
+
let root: string;
|
|
59
|
+
|
|
60
|
+
beforeEach(async () => {
|
|
61
|
+
root = await mkdtemp(join(tmpdir(), "compass-plan-"));
|
|
62
|
+
await mkdir(join(root, "packages", "foo"), { recursive: true });
|
|
63
|
+
await writeFile(join(root, "packages", "foo", "index.ts"), "export const x = 1;\n", "utf8");
|
|
64
|
+
await mkdir(join(root, "missions", "acme-m000001", "workpiece", "src"), {
|
|
65
|
+
recursive: true,
|
|
66
|
+
});
|
|
67
|
+
await writeFile(
|
|
68
|
+
join(root, "missions", "acme-m000001", "workpiece", "src", "local.ts"),
|
|
69
|
+
"export const y = 2;\n",
|
|
70
|
+
"utf8",
|
|
71
|
+
);
|
|
72
|
+
await writeFile(join(root, ".gitignore"), "missions/\n", "utf8");
|
|
73
|
+
await execFileAsync("git", ["init"], { cwd: root });
|
|
74
|
+
await execFileAsync("git", ["config", "user.email", "test@test.com"], { cwd: root });
|
|
75
|
+
await execFileAsync("git", ["config", "user.name", "Test"], { cwd: root });
|
|
76
|
+
await execFileAsync("git", ["add", "."], { cwd: root });
|
|
77
|
+
await execFileAsync("git", ["commit", "-m", "init"], { cwd: root });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
afterEach(async () => {
|
|
81
|
+
await rm(root, { recursive: true, force: true });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("AC-4: due items exclude ledger-ineligible paths and report skippedIneligible", async () => {
|
|
85
|
+
const result = await runCompassAuditPlan(makeInput(), makeContext(root));
|
|
86
|
+
expect(result.exitCode).toBe(0);
|
|
87
|
+
const data = result.data as {
|
|
88
|
+
items: Array<{ path: string }>;
|
|
89
|
+
skippedIneligible: number;
|
|
90
|
+
};
|
|
91
|
+
expect(
|
|
92
|
+
data.items.some((i) => i.path.startsWith("missions/")),
|
|
93
|
+
"plan must not emit work orders for ledger-ineligible paths — check the eligibility partition in runCompassAuditPlan",
|
|
94
|
+
).toBe(false);
|
|
95
|
+
expect(data.items.some((i) => i.path === ELIGIBLE_FILE)).toBe(true);
|
|
96
|
+
expect(data.skippedIneligible).toBeGreaterThan(0);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>
|
|
4
|
+
RFC-1143 AC-5: compass.audit.record on a ledger-ineligible path (missions/,
|
|
5
|
+
gitignored) still writes the entry (operator intent wins) but emits a
|
|
6
|
+
warning that the next baseline run will sweep it.
|
|
7
|
+
</purpose>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
12
|
+
import { mkdtemp, mkdir, writeFile, rm, readFile } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { execFile } from "node:child_process";
|
|
16
|
+
import { promisify } from "node:util";
|
|
17
|
+
import { parse as yamlParse } from "yaml";
|
|
18
|
+
import { runCompassAuditRecord } from "../compass-audit-handler.ts";
|
|
19
|
+
import type { ForgeCommandInput, ForgeRuntimeContext } from "../../../../src/types.ts";
|
|
20
|
+
|
|
21
|
+
const execFileAsync = promisify(execFile);
|
|
22
|
+
|
|
23
|
+
const warnings: string[] = [];
|
|
24
|
+
const logger = {
|
|
25
|
+
section() {},
|
|
26
|
+
info() {},
|
|
27
|
+
warn(msg: string) {
|
|
28
|
+
warnings.push(msg);
|
|
29
|
+
},
|
|
30
|
+
error() {},
|
|
31
|
+
success() {},
|
|
32
|
+
getEvents() {
|
|
33
|
+
return [];
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function makeContext(workspaceRoot: string): ForgeRuntimeContext {
|
|
38
|
+
return {
|
|
39
|
+
workspaceRoot,
|
|
40
|
+
site: undefined,
|
|
41
|
+
siteExplicit: false,
|
|
42
|
+
logger: logger as never,
|
|
43
|
+
dryRun: false,
|
|
44
|
+
outputFormat: "json",
|
|
45
|
+
io: {} as never,
|
|
46
|
+
actualState: undefined as never,
|
|
47
|
+
fileIntents: [],
|
|
48
|
+
} as unknown as ForgeRuntimeContext;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function makeInput(flags: Record<string, unknown> = {}): ForgeCommandInput {
|
|
52
|
+
return { argv: [], flags } as unknown as ForgeCommandInput;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const MISSION_FILE = "missions/acme-m000001/workpiece/src/local.ts";
|
|
56
|
+
|
|
57
|
+
describe("compass.audit.record — RFC-1143 ineligible-path warning", () => {
|
|
58
|
+
let root: string;
|
|
59
|
+
|
|
60
|
+
beforeEach(async () => {
|
|
61
|
+
warnings.length = 0;
|
|
62
|
+
root = await mkdtemp(join(tmpdir(), "compass-record-"));
|
|
63
|
+
await mkdir(join(root, "missions", "acme-m000001", "workpiece", "src"), {
|
|
64
|
+
recursive: true,
|
|
65
|
+
});
|
|
66
|
+
await writeFile(
|
|
67
|
+
join(root, "missions", "acme-m000001", "workpiece", "src", "local.ts"),
|
|
68
|
+
"export const y = 2;\n",
|
|
69
|
+
"utf8",
|
|
70
|
+
);
|
|
71
|
+
await writeFile(join(root, ".gitignore"), "missions/\n", "utf8");
|
|
72
|
+
await execFileAsync("git", ["init"], { cwd: root });
|
|
73
|
+
await execFileAsync("git", ["config", "user.email", "test@test.com"], { cwd: root });
|
|
74
|
+
await execFileAsync("git", ["config", "user.name", "Test"], { cwd: root });
|
|
75
|
+
await execFileAsync("git", ["add", "."], { cwd: root });
|
|
76
|
+
await execFileAsync("git", ["commit", "-m", "init"], { cwd: root });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
afterEach(async () => {
|
|
80
|
+
await rm(root, { recursive: true, force: true });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("AC-5: record on ineligible path writes the entry and warns about the sweep", async () => {
|
|
84
|
+
const absTarget = join(root, MISSION_FILE);
|
|
85
|
+
const result = await runCompassAuditRecord(
|
|
86
|
+
makeInput({ file: absTarget, verdict: "pass", agent: "agent:test" }),
|
|
87
|
+
makeContext(root),
|
|
88
|
+
);
|
|
89
|
+
expect(result.exitCode).toBe(0);
|
|
90
|
+
|
|
91
|
+
// Entry was written despite ineligibility (operator intent wins)
|
|
92
|
+
const ledgerRaw = await readFile(
|
|
93
|
+
join(root, "docs", "compass-audit-ledger.generated.yaml"),
|
|
94
|
+
"utf8",
|
|
95
|
+
);
|
|
96
|
+
const ledger = yamlParse(ledgerRaw) as { entries: Array<{ path: string }> };
|
|
97
|
+
expect(ledger.entries.some((e) => e.path === MISSION_FILE)).toBe(true);
|
|
98
|
+
|
|
99
|
+
// Warning about the next baseline sweep was emitted
|
|
100
|
+
expect(
|
|
101
|
+
warnings.some((w) => w.includes("ledger-ineligible") && w.includes("baseline")),
|
|
102
|
+
"record must warn that an ineligible entry will be swept by the next baseline run",
|
|
103
|
+
).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>
|
|
4
|
+
RFC-1143 AC-1..AC-3: compass.audit.validate must skip ledger-ineligible
|
|
5
|
+
authored paths (missions/, gitignored) instead of emitting COMPASS-AUDIT-01,
|
|
6
|
+
report skippedIneligible/skippedPaths diagnostics, and still flag eligible
|
|
7
|
+
authored files that have no ledger entry.
|
|
8
|
+
</purpose>
|
|
9
|
+
</MODULE_CONTRACT>
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
13
|
+
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { execFile } from "node:child_process";
|
|
17
|
+
import { promisify } from "node:util";
|
|
18
|
+
import { runCompassAuditValidate } from "../compass-audit-handler.ts";
|
|
19
|
+
import type { ForgeCommandInput, ForgeRuntimeContext } from "../../../../src/types.ts";
|
|
20
|
+
|
|
21
|
+
const execFileAsync = promisify(execFile);
|
|
22
|
+
|
|
23
|
+
const logger = {
|
|
24
|
+
section() {},
|
|
25
|
+
info() {},
|
|
26
|
+
warn() {},
|
|
27
|
+
error() {},
|
|
28
|
+
success() {},
|
|
29
|
+
getEvents() {
|
|
30
|
+
return [];
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function makeContext(workspaceRoot: string): ForgeRuntimeContext {
|
|
35
|
+
return {
|
|
36
|
+
workspaceRoot,
|
|
37
|
+
site: undefined,
|
|
38
|
+
siteExplicit: false,
|
|
39
|
+
logger: logger as never,
|
|
40
|
+
dryRun: false,
|
|
41
|
+
outputFormat: "json",
|
|
42
|
+
io: {} as never,
|
|
43
|
+
actualState: undefined as never,
|
|
44
|
+
fileIntents: [],
|
|
45
|
+
} as unknown as ForgeRuntimeContext;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// missions/ is not in the default scanRoots — the real-world failure mode is a
|
|
49
|
+
// scoped scan (--root / site workpiece) that surfaces missions/ paths. Passing
|
|
50
|
+
// both roots reproduces the mixed eligible/ineligible authored set.
|
|
51
|
+
const SCAN_ROOTS = ["missions", "packages"];
|
|
52
|
+
|
|
53
|
+
function makeInput(flags: Record<string, unknown> = {}): ForgeCommandInput {
|
|
54
|
+
return { argv: [], flags: { root: SCAN_ROOTS, ...flags } } as unknown as ForgeCommandInput;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const MISSION_FILE = "missions/acme-m000001/workpiece/src/local.ts";
|
|
58
|
+
const ELIGIBLE_FILE = "packages/foo/index.ts";
|
|
59
|
+
|
|
60
|
+
describe("compass.audit.validate — RFC-1143 ledger-ineligible skips", () => {
|
|
61
|
+
let root: string;
|
|
62
|
+
|
|
63
|
+
beforeEach(async () => {
|
|
64
|
+
root = await mkdtemp(join(tmpdir(), "compass-validate-"));
|
|
65
|
+
// Authored source file (plain .ts, no generated marker → authored entry)
|
|
66
|
+
await mkdir(join(root, "packages", "foo"), { recursive: true });
|
|
67
|
+
await writeFile(join(root, "packages", "foo", "index.ts"), "export const x = 1;\n", "utf8");
|
|
68
|
+
// Mission workpiece file — authored but ledger-ineligible
|
|
69
|
+
await mkdir(join(root, "missions", "acme-m000001", "workpiece", "src"), {
|
|
70
|
+
recursive: true,
|
|
71
|
+
});
|
|
72
|
+
await writeFile(
|
|
73
|
+
join(root, "missions", "acme-m000001", "workpiece", "src", "local.ts"),
|
|
74
|
+
"export const y = 2;\n",
|
|
75
|
+
"utf8",
|
|
76
|
+
);
|
|
77
|
+
await writeFile(join(root, ".gitignore"), "missions/\n", "utf8");
|
|
78
|
+
// Git repo for getRevisionByPath + check-ignore
|
|
79
|
+
await execFileAsync("git", ["init"], { cwd: root });
|
|
80
|
+
await execFileAsync("git", ["config", "user.email", "test@test.com"], { cwd: root });
|
|
81
|
+
await execFileAsync("git", ["config", "user.name", "Test"], { cwd: root });
|
|
82
|
+
await execFileAsync("git", ["add", "."], { cwd: root });
|
|
83
|
+
await execFileAsync("git", ["commit", "-m", "init"], { cwd: root });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
afterEach(async () => {
|
|
87
|
+
await rm(root, { recursive: true, force: true });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("AC-1: no COMPASS-AUDIT-01 diagnostic for missions/ paths", async () => {
|
|
91
|
+
const result = await runCompassAuditValidate(makeInput(), makeContext(root));
|
|
92
|
+
const diagnostics = (result.data as { diagnostics: Array<{ file: string; ruleId: string }> })
|
|
93
|
+
.diagnostics;
|
|
94
|
+
expect(
|
|
95
|
+
diagnostics.some((d) => d.file.startsWith("missions/")),
|
|
96
|
+
"validate must not flag ledger-ineligible missions/ paths — check the eligibility partition in runCompassAuditValidate",
|
|
97
|
+
).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("AC-2: result reports skippedIneligible > 0 and skippedPaths", async () => {
|
|
101
|
+
const result = await runCompassAuditValidate(makeInput(), makeContext(root));
|
|
102
|
+
const data = result.data as { skippedIneligible: number; skippedPaths: string[] };
|
|
103
|
+
expect(data.skippedIneligible).toBeGreaterThan(0);
|
|
104
|
+
expect(data.skippedPaths).toContain(MISSION_FILE);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("AC-3: eligible authored file without ledger entry still emits COMPASS-AUDIT-01", async () => {
|
|
108
|
+
const result = await runCompassAuditValidate(makeInput(), makeContext(root));
|
|
109
|
+
const diagnostics = (result.data as { diagnostics: Array<{ file: string; ruleId: string }> })
|
|
110
|
+
.diagnostics;
|
|
111
|
+
expect(
|
|
112
|
+
diagnostics.some((d) => d.file === ELIGIBLE_FILE && d.ruleId === "COMPASS-AUDIT-01"),
|
|
113
|
+
"eligible authored files must still be audited — check that only ineligible paths were partitioned out",
|
|
114
|
+
).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("AC-1 (strict): exits 0 when only ineligible paths would be due", async () => {
|
|
118
|
+
// Seed a ledger entry for the eligible file at its current revision (1
|
|
119
|
+
// commit → revision 1) so only the missions/ file would be due.
|
|
120
|
+
await mkdir(join(root, "docs"), { recursive: true });
|
|
121
|
+
await writeFile(
|
|
122
|
+
join(root, "docs", "compass-audit-ledger.generated.yaml"),
|
|
123
|
+
`entries:\n - path: ${ELIGIBLE_FILE}\n entityId: ""\n auditedRevision: 1\n auditedHash: ""\n auditedAt: "2026-01-01T00:00:00Z"\n verdict: pass\n agent: human:test\n`,
|
|
124
|
+
"utf8",
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const result = await runCompassAuditValidate(makeInput({ strict: true }), makeContext(root));
|
|
128
|
+
expect(result.exitCode).toBe(0);
|
|
129
|
+
const data = result.data as { dueCount: number; skippedIneligible: number };
|
|
130
|
+
expect(data.dueCount).toBe(0);
|
|
131
|
+
expect(data.skippedIneligible).toBeGreaterThan(0);
|
|
132
|
+
});
|
|
133
|
+
});
|