peaks-loop 4.0.48 → 4.0.50
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 +44 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/audit-commands.js +1 -0
- package/dist/cli/commands/baseline-commands.js +163 -25
- package/dist/cli/commands/compact-command.js +1 -3
- package/dist/cli/commands/core/skill-command.js +53 -4
- package/dist/cli/commands/core/standards-command.d.ts +24 -0
- package/dist/cli/commands/core/standards-command.js +74 -0
- package/dist/cli/commands/feedback-commands.d.ts +11 -7
- package/dist/cli/commands/feedback-commands.js +49 -17
- package/dist/cli/commands/final-review-commands.js +12 -0
- package/dist/cli/commands/hooks-commands.js +55 -38
- package/dist/cli/commands/loop-eval-commands.js +22 -6
- package/dist/cli/commands/share-commands.js +37 -11
- package/dist/cli/commands/slice-integrate-commands.js +17 -0
- package/dist/cli/commands/web-commands.js +8 -1
- package/dist/cli/commands/workflow-lifecycle-commands.d.ts +6 -0
- package/dist/cli/commands/workflow-lifecycle-commands.js +64 -3
- package/dist/services/adapter/adapter.d.ts +30 -0
- package/dist/services/adapter/auto-adapter.d.ts +13 -0
- package/dist/services/adapter/claude-adapter.js +12 -0
- package/dist/services/adapter/codex-adapter.d.ts +12 -0
- package/dist/services/adapter/codex-adapter.js +12 -0
- package/dist/services/adapter/copilot-adapter.d.ts +12 -0
- package/dist/services/adapter/copilot-adapter.js +12 -0
- package/dist/services/artifacts/artifact-prerequisites.js +10 -0
- package/dist/services/artifacts/request-artifact-service.js +59 -38
- package/dist/services/audit/backing-detector.d.ts +25 -7
- package/dist/services/audit/backing-detector.js +33 -17
- package/dist/services/audit/enforcer-liveness.d.ts +12 -0
- package/dist/services/audit/enforcer-liveness.js +100 -0
- package/dist/services/audit/enforcers/active-skill-resolver.js +14 -1
- package/dist/services/audit/enforcers/lint-catalog-governance.d.ts +23 -11
- package/dist/services/audit/enforcers/lint-catalog-governance.js +10 -14
- package/dist/services/audit/enforcers/lint-rd-handoff-coverage.d.ts +5 -15
- package/dist/services/audit/enforcers/lint-rd-handoff-coverage.js +94 -25
- package/dist/services/audit/enforcers/lint-style.d.ts +9 -1
- package/dist/services/audit/enforcers/lint-style.js +38 -2
- package/dist/services/audit/prose-ratio-calculator.d.ts +28 -17
- package/dist/services/audit/prose-ratio-calculator.js +25 -18
- package/dist/services/audit/red-line-catalog-p2-a.js +1 -1
- package/dist/services/audit/red-lines-service.js +51 -7
- package/dist/services/capability-audit-service/independent-checker.d.ts +15 -0
- package/dist/services/capability-audit-service/independent-checker.js +140 -0
- package/dist/services/capability-audit-service/index.d.ts +3 -1
- package/dist/services/capability-audit-service/index.js +1 -0
- package/dist/services/capability-audit-service/runner.d.ts +17 -13
- package/dist/services/capability-audit-service/runner.js +76 -15
- package/dist/services/capability-audit-service/types.d.ts +48 -0
- package/dist/services/capability-guard-runner/contracts/J01.js +21 -22
- package/dist/services/capability-guard-runner/contracts/J02.d.ts +1 -1
- package/dist/services/capability-guard-runner/contracts/J02.js +114 -28
- package/dist/services/capability-guard-runner/contracts/J03.d.ts +13 -0
- package/dist/services/capability-guard-runner/contracts/J03.js +72 -21
- package/dist/services/capability-guard-runner/contracts/J04.d.ts +6 -0
- package/dist/services/capability-guard-runner/contracts/J04.js +65 -32
- package/dist/services/capability-guard-runner/contracts/J05.js +118 -16
- package/dist/services/capability-guard-runner/contracts/J06.d.ts +14 -0
- package/dist/services/capability-guard-runner/contracts/J06.js +57 -39
- package/dist/services/capability-guard-runner/contracts/J07.d.ts +9 -0
- package/dist/services/capability-guard-runner/contracts/J07.js +76 -47
- package/dist/services/capability-guard-runner/contracts/J08.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J08.js +66 -39
- package/dist/services/capability-guard-runner/contracts/J09.d.ts +13 -0
- package/dist/services/capability-guard-runner/contracts/J09.js +95 -39
- package/dist/services/capability-guard-runner/contracts/J10.d.ts +12 -0
- package/dist/services/capability-guard-runner/contracts/J10.js +69 -35
- package/dist/services/capability-guard-runner/contracts/J11.d.ts +8 -0
- package/dist/services/capability-guard-runner/contracts/J11.js +73 -33
- package/dist/services/capability-guard-runner/contracts/J12.d.ts +12 -0
- package/dist/services/capability-guard-runner/contracts/J12.js +66 -30
- package/dist/services/capability-guard-runner/contracts/J13.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J13.js +62 -40
- package/dist/services/capability-guard-runner/contracts/J14.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J14.js +60 -31
- package/dist/services/capability-guard-runner/contracts/J15.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J15.js +70 -35
- package/dist/services/capability-guard-runner/contracts/_shared.d.ts +24 -0
- package/dist/services/capability-guard-runner/contracts/_shared.js +67 -0
- package/dist/services/capability-guard-runner/registry.d.ts +5 -0
- package/dist/services/capability-guard-runner/registry.js +140 -0
- package/dist/services/capability-guard-runner/runner.d.ts +26 -0
- package/dist/services/capability-guard-runner/runner.js +63 -6
- package/dist/services/code/auto-compact-lifecycle.d.ts +75 -0
- package/dist/services/code/auto-compact-lifecycle.js +65 -16
- package/dist/services/code/auto-compact-modes.d.ts +13 -2
- package/dist/services/code/auto-compact-modes.js +20 -4
- package/dist/services/code/auto-compact-orchestrator.js +119 -19
- package/dist/services/code/compact-event-settle.d.ts +20 -8
- package/dist/services/code/compact-event-settle.js +21 -0
- package/dist/services/code/post-compact-detector.js +20 -11
- package/dist/services/code/step-08-gate.js +21 -6
- package/dist/services/compact-statusline/compact-statusline-service.js +56 -22
- package/dist/services/config/config-safety.js +11 -9
- package/dist/services/context/auto-compact-types.d.ts +20 -2
- package/dist/services/feedback/feedback-promotion-service.d.ts +137 -14
- package/dist/services/feedback/feedback-promotion-service.js +341 -20
- package/dist/services/feedback/promotion-artifact-evidence.d.ts +69 -0
- package/dist/services/feedback/promotion-artifact-evidence.js +332 -0
- package/dist/services/final-review/pre-post-diff.js +10 -2
- package/dist/services/job/job-progress-store.js +18 -3
- package/dist/services/observability/jsonl-store.d.ts +19 -0
- package/dist/services/observability/jsonl-store.js +27 -2
- package/dist/services/observability/observability-service.d.ts +11 -4
- package/dist/services/observability/observability-service.js +16 -3
- package/dist/services/prd/handoff-service.js +43 -0
- package/dist/services/qa/qa-business-review-state.js +19 -5
- package/dist/services/sc/sc-service.d.ts +8 -0
- package/dist/services/sc/sc-service.js +8 -1
- package/dist/services/scan/api-diff-types.js +20 -2
- package/dist/services/security/safe-settings-path.js +19 -1
- package/dist/services/session/getSessionDir.d.ts +33 -0
- package/dist/services/session/getSessionDir.js +60 -0
- package/dist/services/skill/skill-search-service.d.ts +3 -3
- package/dist/services/slice/slice-review-state.js +19 -4
- package/dist/services/standards/loop-engineering-lint.d.ts +1 -1
- package/dist/services/standards/loop-engineering-lint.js +6 -0
- package/dist/services/web/daemon-registry.js +27 -2
- package/dist/services/workflow/pipeline-verify-gate-support.js +10 -11
- package/dist/services/workflow/pipeline-verify-service.d.ts +1 -1
- package/dist/services/workflow/pipeline-verify-service.js +23 -10
- package/dist/services/workflow/pipeline-verify-types.d.ts +5 -3
- package/dist/services/workspace/claude-settings-template.d.ts +53 -37
- package/dist/services/workspace/claude-settings-template.js +105 -83
- package/dist/services/workspace/generated-artifacts-stamp.d.ts +119 -0
- package/dist/services/workspace/generated-artifacts-stamp.js +167 -0
- package/dist/services/workspace/workspace-claude-settings-materializer.d.ts +8 -0
- package/dist/services/workspace/workspace-claude-settings-materializer.js +38 -3
- package/dist/services/workspace/workspace-service.js +11 -1
- package/dist/shared/fs-utils.d.ts +26 -0
- package/dist/shared/fs-utils.js +35 -0
- package/dist/shared/runtime-root.d.ts +73 -0
- package/dist/shared/runtime-root.js +77 -0
- package/package.json +9 -7
- package/scripts/copy-templates.mjs +0 -12
- package/scripts/install-skills.mjs +154 -53
- package/skills/bee/peaks-qa/SKILL.md +0 -1
- package/skills/bee/peaks-rd/SKILL.md +0 -1
- package/skills/peaks-code/SKILL.md +12 -10
- package/skills/peaks-code/references/periodic-checkpoint.md +2 -2
- package/skills/peaks-code/references/runbook.md +3 -0
- package/skills/peaks-code/references/session-overload-signal-index.md +4 -2
- package/skills/peaks-code/references/startup-sequence.md +2 -2
- package/skills/peaks-code/references/step-0-8-gate.md +1 -1
- package/skills/peaks-code/references/sub-agent-dispatch.md +19 -19
- package/dist/cli/commands/context-builder-commands.d.ts +0 -11
- package/dist/cli/commands/context-builder-commands.js +0 -85
- package/dist/services/hooks/write-gate.js +0 -111
- package/skills/bee/peaks-prd/references/command-migration.md +0 -3
- package/skills/bee/peaks-qa/references/command-migration.md +0 -3
- package/skills/bee/peaks-rd/references/command-migration.md +0 -3
- package/skills/bee/peaks-sc/references/command-migration.md +0 -3
- package/skills/bee/peaks-txt/references/command-migration.md +0 -3
- package/skills/bee/peaks-ui/references/command-migration.md +0 -3
- package/skills/peaks-code/references/command-migration.md +0 -3
|
@@ -1,43 +1,70 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { CrystallizationIntegrityError, CrystallizationService, CrystallizationTaskStateSchema } from '../../crystallization/crystallization-service.js';
|
|
3
|
+
import { combineProbes, fail, missingSourceFiles, pass, probe, requireBaselineRow } from './_shared.js';
|
|
4
|
+
/** Test doubles: `assertReady` never reaches any of them. */
|
|
5
|
+
function probeService() {
|
|
6
|
+
const noop = () => undefined;
|
|
7
|
+
const opts = {
|
|
8
|
+
loopReleaseSchema: { parse: (input) => input },
|
|
9
|
+
loopBeeRelationSchema: { parse: (input) => input, omit: () => ({ parse: (input) => input }) },
|
|
10
|
+
insertLoopRelease: noop,
|
|
11
|
+
insertLoopBeeRelation: noop
|
|
12
|
+
};
|
|
13
|
+
return new CrystallizationService(new Database(':memory:'), opts);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Behavioural probe of the crystallization gate.
|
|
17
|
+
*
|
|
18
|
+
* Before this the contract only asserted that one of three candidate files
|
|
19
|
+
* exists and mentions "crystalliz"/"sediment"/"promot" — the file it found is
|
|
20
|
+
* named `crystallization-service.ts`, so the check restated its own filename.
|
|
21
|
+
*
|
|
22
|
+
* Here the real gate is driven with a completed and an incomplete task state:
|
|
23
|
+
* a task whose `gates_passed` is false must be refused at the Zod boundary AND
|
|
24
|
+
* again at the service boundary.
|
|
25
|
+
*/
|
|
12
26
|
export async function runJ08Contract(ctx) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
const row = requireBaselineRow(ctx);
|
|
28
|
+
const missing = missingSourceFiles(ctx, row);
|
|
29
|
+
const ready = { task_id: 'guard-J08', task_status: 'completed', gates_passed: true, evidence_collected: true };
|
|
30
|
+
const schemaAcceptsReady = CrystallizationTaskStateSchema.safeParse(ready).success;
|
|
31
|
+
const schemaRejectsGatesFalse = !CrystallizationTaskStateSchema.safeParse({ ...ready, gates_passed: false }).success;
|
|
32
|
+
const schemaRejectsNotCompleted = !CrystallizationTaskStateSchema.safeParse({ ...ready, task_status: 'in_progress' }).success;
|
|
33
|
+
const schemaRejectsNoEvidence = !CrystallizationTaskStateSchema.safeParse({ ...ready, evidence_collected: false }).success;
|
|
34
|
+
const svc = probeService();
|
|
35
|
+
let serviceAcceptsReady = false;
|
|
36
|
+
try {
|
|
37
|
+
serviceAcceptsReady = svc.assertReady(ready).task_id === 'guard-J08';
|
|
22
38
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const src = readFileSync(join(ctx.projectRoot, f), 'utf8').toLowerCase();
|
|
26
|
-
return src.includes('crystallization') || src.includes('sediment') || src.includes('promot');
|
|
27
|
-
});
|
|
28
|
-
if (!primary) {
|
|
29
|
-
return {
|
|
30
|
-
journeyId: 'J08',
|
|
31
|
-
contract: 'asset-roundtrip',
|
|
32
|
-
status: 'fail',
|
|
33
|
-
diff: { before: 'crystallization service references sediment/promotion', after: 'no service mentions any of them', reason: 'J08#1 broken: crystallization service is not wired to sediment' },
|
|
34
|
-
artifactPath: existing[0]
|
|
35
|
-
};
|
|
39
|
+
catch {
|
|
40
|
+
serviceAcceptsReady = false;
|
|
36
41
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
const refused = [];
|
|
43
|
+
const badStates = [
|
|
44
|
+
['gates_passed=false', { ...ready, gates_passed: false }],
|
|
45
|
+
['task_status=in_progress', { ...ready, task_status: 'in_progress' }],
|
|
46
|
+
['evidence_collected=false', { ...ready, evidence_collected: false }]
|
|
47
|
+
];
|
|
48
|
+
for (const [label, task] of badStates) {
|
|
49
|
+
try {
|
|
50
|
+
svc.assertReady(task);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
if (e instanceof CrystallizationIntegrityError)
|
|
54
|
+
refused.push(label);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const result = combineProbes([
|
|
58
|
+
probe(missing.length === 0, `baseline sourceFiles present (${row.sourceFiles.length})`),
|
|
59
|
+
probe(schemaAcceptsReady, 'a completed, gate-passed, evidenced task passes the schema'),
|
|
60
|
+
probe(schemaRejectsGatesFalse, 'gates_passed=false is refused by the schema'),
|
|
61
|
+
probe(schemaRejectsNotCompleted, 'task_status!=completed is refused by the schema'),
|
|
62
|
+
probe(schemaRejectsNoEvidence, 'evidence_collected=false is refused by the schema'),
|
|
63
|
+
probe(serviceAcceptsReady, 'the service accepts a ready task'),
|
|
64
|
+
probe(refused.length === 3, `the service refuses every non-passing gate state with CrystallizationIntegrityError (refused: ${refused.join(', ') || 'none'})`)
|
|
65
|
+
]);
|
|
66
|
+
const artifact = row.sourceFiles[0] ?? 'src/services/crystallization/crystallization-service.ts';
|
|
67
|
+
if (result.ok)
|
|
68
|
+
return pass(ctx, artifact);
|
|
69
|
+
return fail(ctx, artifact, 'crystallization refuses any task whose gates are not in a passing state', result.detail, 'J08 invariant broken: crystallization no longer enforces the pre-run gate');
|
|
43
70
|
}
|
|
@@ -1,2 +1,15 @@
|
|
|
1
1
|
import type { GuardContext, GuardRunResult } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Behavioural probe of the SOP id guard and the lint-before-register ordering.
|
|
4
|
+
*
|
|
5
|
+
* The previous version listed five SOP files and passed if any of them
|
|
6
|
+
* mentioned "sop", "gate" or "register" — every file in the list is named
|
|
7
|
+
* `sop-*.ts`, so the check could not fail.
|
|
8
|
+
*
|
|
9
|
+
* Here the real `initSop` / `registerSop` path runs against a tmp project:
|
|
10
|
+
* path-traversal and reserved ids must be refused, and a manifest that fails
|
|
11
|
+
* lint must stop the registration BEFORE `registry.json` is written. The
|
|
12
|
+
* positive control (a clean manifest DOES register) is what makes the negative
|
|
13
|
+
* assertion mean something.
|
|
14
|
+
*/
|
|
2
15
|
export declare function runJ09Contract(ctx: GuardContext): Promise<GuardRunResult>;
|
|
@@ -1,44 +1,100 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
'
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
import { initSop } from '../../sop/sop-service.js';
|
|
5
|
+
import { SOP_ID_PATTERN } from '../../sop/sop-types.js';
|
|
6
|
+
import { registerSop, SopRegisterError } from '../../sop/sop-registry-service.js';
|
|
7
|
+
import { combineProbes, fail, missingSourceFiles, pass, probe, requireBaselineRow } from './_shared.js';
|
|
8
|
+
const VALID_ID = 'guard-demo';
|
|
9
|
+
function registryPath(projectRoot) {
|
|
10
|
+
return join(projectRoot, '.peaks', 'sops', 'registry.json');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Behavioural probe of the SOP id guard and the lint-before-register ordering.
|
|
14
|
+
*
|
|
15
|
+
* The previous version listed five SOP files and passed if any of them
|
|
16
|
+
* mentioned "sop", "gate" or "register" — every file in the list is named
|
|
17
|
+
* `sop-*.ts`, so the check could not fail.
|
|
18
|
+
*
|
|
19
|
+
* Here the real `initSop` / `registerSop` path runs against a tmp project:
|
|
20
|
+
* path-traversal and reserved ids must be refused, and a manifest that fails
|
|
21
|
+
* lint must stop the registration BEFORE `registry.json` is written. The
|
|
22
|
+
* positive control (a clean manifest DOES register) is what makes the negative
|
|
23
|
+
* assertion mean something.
|
|
24
|
+
*/
|
|
13
25
|
export async function runJ09Contract(ctx) {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
const row = requireBaselineRow(ctx);
|
|
27
|
+
const missing = missingSourceFiles(ctx, row);
|
|
28
|
+
const root = mkdtempSync(join(tmpdir(), 'cbl-J09-'));
|
|
29
|
+
try {
|
|
30
|
+
const patternRejects = ['a.b', 'a/b', '../escape', 'Bad-Id', ''].every((id) => !SOP_ID_PATTERN.test(id));
|
|
31
|
+
const patternAccepts = ['demo', 'my-sop', 'a', 'sop2'].every((id) => SOP_ID_PATTERN.test(id));
|
|
32
|
+
const reservedRefused = [];
|
|
33
|
+
for (const id of ['peaks-x', 'peaks']) {
|
|
34
|
+
try {
|
|
35
|
+
await initSop({ id, projectRoot: root, apply: true });
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
if (/reserved/i.test(e.message))
|
|
39
|
+
reservedRefused.push(id);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
let traversalRefused = false;
|
|
43
|
+
try {
|
|
44
|
+
await initSop({ id: '../escape', projectRoot: root, apply: true });
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
traversalRefused = /invalid sop id|pattern|kebab/i.test(e.message);
|
|
48
|
+
}
|
|
49
|
+
// Positive control: a real SOP initialises and registers cleanly.
|
|
50
|
+
let cleanRegisters = false;
|
|
51
|
+
let registerError = '';
|
|
52
|
+
try {
|
|
53
|
+
await initSop({ id: VALID_ID, projectRoot: root, apply: true });
|
|
54
|
+
const reg = await registerSop({ id: VALID_ID, projectRoot: root });
|
|
55
|
+
cleanRegisters = reg.applied === true && existsSync(registryPath(root));
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
registerError = `${e.name}: ${e.message.slice(0, 140)}`;
|
|
59
|
+
}
|
|
60
|
+
// Negative control: break the manifest so lint must fail, then register.
|
|
61
|
+
let dirtyRefused = false;
|
|
62
|
+
let dirtyCode = 'none';
|
|
63
|
+
rmSync(registryPath(root), { force: true });
|
|
64
|
+
const manifestPath = join(root, '.peaks', 'sops', VALID_ID, 'sop.json');
|
|
65
|
+
if (existsSync(manifestPath)) {
|
|
66
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
67
|
+
manifest['id'] = 'not-the-directory-id';
|
|
68
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
mkdirSync(join(root, '.peaks', 'sops', VALID_ID), { recursive: true });
|
|
72
|
+
writeFileSync(manifestPath, JSON.stringify({ id: 'not-the-directory-id', name: 'x', phases: ['p'], gates: [] }));
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
await registerSop({ id: VALID_ID, projectRoot: root });
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
dirtyCode = e instanceof SopRegisterError ? e.code : e.name;
|
|
79
|
+
dirtyRefused = dirtyCode === 'SOP_INVALID';
|
|
80
|
+
}
|
|
81
|
+
const wroteRegistryDespiteLintFailure = existsSync(registryPath(root));
|
|
82
|
+
const result = combineProbes([
|
|
83
|
+
probe(missing.length === 0, `baseline sourceFiles present (${row.sourceFiles.length})`),
|
|
84
|
+
probe(patternRejects, 'SOP_ID_PATTERN rejects dots, slashes, traversal and empty ids'),
|
|
85
|
+
probe(patternAccepts, 'SOP_ID_PATTERN accepts lowercase kebab ids'),
|
|
86
|
+
probe(reservedRefused.length === 2, `reserved ids are refused (refused: ${reservedRefused.join(',') || 'none'})`),
|
|
87
|
+
probe(traversalRefused, "initSop refuses '../escape'"),
|
|
88
|
+
probe(cleanRegisters, `a clean SOP registers and writes registry.json${registerError ? ` [${registerError}]` : ''}`),
|
|
89
|
+
probe(dirtyRefused, `a manifest that fails lint is refused with SOP_INVALID (saw ${dirtyCode})`),
|
|
90
|
+
probe(!wroteRegistryDespiteLintFailure, 'lint failure halts registration before registry.json is written')
|
|
91
|
+
]);
|
|
92
|
+
const artifact = row.sourceFiles[0] ?? 'src/services/sop/sop-service.ts';
|
|
93
|
+
if (result.ok)
|
|
94
|
+
return pass(ctx, artifact);
|
|
95
|
+
return fail(ctx, artifact, 'SOP ids are pattern-guarded and lintSop() gates registerSop()', result.detail, 'J09 invariant broken: the SOP id guard or the lint-before-register ordering no longer holds');
|
|
23
96
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const src = readFileSync(join(ctx.projectRoot, f), 'utf8').toLowerCase();
|
|
27
|
-
return src.includes('sop') || src.includes('gate') || src.includes('register');
|
|
28
|
-
});
|
|
29
|
-
if (!primary) {
|
|
30
|
-
return {
|
|
31
|
-
journeyId: 'J09',
|
|
32
|
-
contract: 'sop-register',
|
|
33
|
-
status: 'fail',
|
|
34
|
-
diff: { before: 'SOP service references sop/gate/register', after: 'no service mentions any of them', reason: 'J09#1 broken: SOP service is not wired to gate-able registration' },
|
|
35
|
-
artifactPath: existing[0]
|
|
36
|
-
};
|
|
97
|
+
finally {
|
|
98
|
+
rmSync(root, { recursive: true, force: true });
|
|
37
99
|
}
|
|
38
|
-
return {
|
|
39
|
-
journeyId: 'J09',
|
|
40
|
-
contract: 'sop-register',
|
|
41
|
-
status: 'pass',
|
|
42
|
-
artifactPath: existing[0]
|
|
43
|
-
};
|
|
44
100
|
}
|
|
@@ -1,2 +1,14 @@
|
|
|
1
1
|
import type { GuardContext, GuardRunResult } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Behavioural probe of "a plan is produced before anything is written, so the
|
|
4
|
+
* diff is reviewable".
|
|
5
|
+
*
|
|
6
|
+
* The previous version listed four hook/IDE files and passed if any of them
|
|
7
|
+
* contained the word "hook", "install", "ide" or "adapter" — `hooks-commands.ts`
|
|
8
|
+
* satisfies that from its own filename.
|
|
9
|
+
*
|
|
10
|
+
* Here `planHookInstall` runs against a fresh tmp project and the settings file
|
|
11
|
+
* must NOT appear; `applyHookInstall` then writes it, and a second apply must
|
|
12
|
+
* be a no-op that does not duplicate the entry.
|
|
13
|
+
*/
|
|
2
14
|
export declare function runJ10Contract(ctx: GuardContext): Promise<GuardRunResult>;
|
|
@@ -1,40 +1,74 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
import { applyHookInstall, planHookInstall } from '../../skills/hooks-settings-service.js';
|
|
5
|
+
import { combineProbes, fail, missingSourceFiles, pass, probe, requireBaselineRow } from './_shared.js';
|
|
6
|
+
import { normalizePath } from '../../../shared/path-utils.js';
|
|
7
|
+
function countOccurrences(haystack, needle) {
|
|
8
|
+
if (needle.length === 0)
|
|
9
|
+
return 0;
|
|
10
|
+
let count = 0;
|
|
11
|
+
let from = 0;
|
|
12
|
+
for (;;) {
|
|
13
|
+
const at = haystack.indexOf(needle, from);
|
|
14
|
+
if (at < 0)
|
|
15
|
+
return count;
|
|
16
|
+
count += 1;
|
|
17
|
+
from = at + needle.length;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Behavioural probe of "a plan is produced before anything is written, so the
|
|
22
|
+
* diff is reviewable".
|
|
23
|
+
*
|
|
24
|
+
* The previous version listed four hook/IDE files and passed if any of them
|
|
25
|
+
* contained the word "hook", "install", "ide" or "adapter" — `hooks-commands.ts`
|
|
26
|
+
* satisfies that from its own filename.
|
|
27
|
+
*
|
|
28
|
+
* Here `planHookInstall` runs against a fresh tmp project and the settings file
|
|
29
|
+
* must NOT appear; `applyHookInstall` then writes it, and a second apply must
|
|
30
|
+
* be a no-op that does not duplicate the entry.
|
|
31
|
+
*/
|
|
10
32
|
export async function runJ10Contract(ctx) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
33
|
+
const row = requireBaselineRow(ctx);
|
|
34
|
+
const missing = missingSourceFiles(ctx, row);
|
|
35
|
+
const root = mkdtempSync(join(tmpdir(), 'cbl-J10-'));
|
|
36
|
+
try {
|
|
37
|
+
const plan = planHookInstall('project', root);
|
|
38
|
+
const settingsPath = plan.settingsPath;
|
|
39
|
+
const plannedInsideProject = normalizePath(settingsPath).startsWith(normalizePath(root));
|
|
40
|
+
const planIsReadOnly = !existsSync(settingsPath);
|
|
41
|
+
const freshProjectNotInstalled = plan.exists === false && plan.alreadyInstalled === false;
|
|
42
|
+
// Each managed hook entry is identified by its own sentinel; every one of
|
|
43
|
+
// them must land exactly once and stay at one across a re-apply.
|
|
44
|
+
const sentinels = plan.entryTargets.filter((t) => t.settingsPath === settingsPath).map((t) => t.sentinel);
|
|
45
|
+
const countsNow = () => existsSync(settingsPath) ? sentinels.map((s) => countOccurrences(readFileSync(settingsPath, 'utf8'), s)) : [];
|
|
46
|
+
const first = applyHookInstall('project', root);
|
|
47
|
+
const fileWritten = existsSync(settingsPath);
|
|
48
|
+
const installedCounts = countsNow();
|
|
49
|
+
const sentinelCount = installedCounts.length > 0 ? installedCounts[0] : -1;
|
|
50
|
+
const second = applyHookInstall('project', root);
|
|
51
|
+
const afterSecondApply = countsNow().length > 0 ? countsNow()[0] : -1;
|
|
52
|
+
const planAfterApply = planHookInstall('project', root);
|
|
53
|
+
const result = combineProbes([
|
|
54
|
+
probe(missing.length === 0, `baseline sourceFiles present (${row.sourceFiles.length})`),
|
|
55
|
+
probe(plannedInsideProject, `the plan targets a settings file inside the project (${settingsPath})`),
|
|
56
|
+
probe(freshProjectNotInstalled, 'a fresh project reports exists=false / alreadyInstalled=false'),
|
|
57
|
+
probe(planIsReadOnly, 'planning does not create the settings file — the diff is reviewable first'),
|
|
58
|
+
probe(first.applied === true, 'apply reports applied=true'),
|
|
59
|
+
probe(fileWritten, 'apply writes the settings file'),
|
|
60
|
+
probe(sentinels.length > 0, `the plan enumerates managed hook entries (${String(sentinels.length)})`),
|
|
61
|
+
probe(installedCounts.length > 0 && installedCounts.every((c) => c === 1), `apply installs every managed entry exactly once (counts=${installedCounts.join(',') || 'none'})`),
|
|
62
|
+
probe(second.applied === false, `a second apply is a no-op (applied=${String(second.applied)}, first sentinel count=${String(sentinelCount)})`),
|
|
63
|
+
probe(afterSecondApply === 1, `a duplicate apply does not duplicate the entry (count=${String(afterSecondApply)})`),
|
|
64
|
+
probe(planAfterApply.alreadyInstalled === true, 'the plan reports alreadyInstalled=true after a successful apply')
|
|
65
|
+
]);
|
|
66
|
+
const artifact = row.sourceFiles[0] ?? 'src/services/skills/hooks-settings-service.ts';
|
|
67
|
+
if (result.ok)
|
|
68
|
+
return pass(ctx, artifact);
|
|
69
|
+
return fail(ctx, artifact, 'planHookInstall() produces a reviewable plan before applyHookInstall() writes, and re-applying is a no-op', result.detail, 'J10 invariant broken: hook install is no longer plan-then-apply, or is not idempotent');
|
|
20
70
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return src.includes('hook') || src.includes('install') || src.includes('ide') || src.includes('adapter');
|
|
24
|
-
});
|
|
25
|
-
if (!primary) {
|
|
26
|
-
return {
|
|
27
|
-
journeyId: 'J10',
|
|
28
|
-
contract: 'hook-assertion',
|
|
29
|
-
status: 'fail',
|
|
30
|
-
diff: { before: 'hooks/ide commands reference install/hook/ide/adapter', after: 'no such reference', reason: 'J10#1 broken: hooks/ide commands do not implement install/hook/ide/adapter' },
|
|
31
|
-
artifactPath: existing[0]
|
|
32
|
-
};
|
|
71
|
+
finally {
|
|
72
|
+
rmSync(root, { recursive: true, force: true });
|
|
33
73
|
}
|
|
34
|
-
return {
|
|
35
|
-
journeyId: 'J10',
|
|
36
|
-
contract: 'hook-assertion',
|
|
37
|
-
status: 'pass',
|
|
38
|
-
artifactPath: existing[0]
|
|
39
|
-
};
|
|
40
74
|
}
|
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
import type { GuardContext, GuardRunResult } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Behavioural probe of "doctor checks live under a code-driven fixed-registry
|
|
4
|
+
* tree, and each openspec change is generated from a real doctor finding".
|
|
5
|
+
*
|
|
6
|
+
* The previous version listed doctor/audit/openspec files and passed if any of
|
|
7
|
+
* them mentioned "doctor", "audit", "openspec" or "health" — `doctor-service.ts`
|
|
8
|
+
* satisfies that from its own filename.
|
|
9
|
+
*/
|
|
2
10
|
export declare function runJ11Contract(ctx: GuardContext): Promise<GuardRunResult>;
|
|
@@ -1,39 +1,79 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import { PLUGINS } from '../../doctor/doctor-service/plugin-registry.js';
|
|
5
|
+
import { proposeFromDoctor } from '../../openspec/openspec-propose-from-doctor-service.js';
|
|
6
|
+
import { combineProbes, fail, missingSourceFiles, pass, probe, requireBaselineRow } from './_shared.js';
|
|
7
|
+
const CHECKS_DIR = ['src', 'services', 'doctor', 'doctor-service', 'checks'];
|
|
8
|
+
const LEGACY_SHIM = ['src', 'services', 'doctor', 'doctor-service.ts'];
|
|
9
|
+
const FROZEN_CLOCK = () => '2020-01-01T00:00:00.000Z';
|
|
10
|
+
/** Strip block + line comments so only executable lines remain. */
|
|
11
|
+
function executableLines(source) {
|
|
12
|
+
return source
|
|
13
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
14
|
+
.split('\n')
|
|
15
|
+
.map((line) => line.replace(/\/\/.*$/, '').trim())
|
|
16
|
+
.filter((line) => line.length > 0);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Constructs a re-export-only shim must not contain. `export * from` /
|
|
20
|
+
* `export { a, b } from` / `export type { ... }` are the only allowed forms;
|
|
21
|
+
* anything below means logic moved back into the legacy path.
|
|
22
|
+
*/
|
|
23
|
+
const FORBIDDEN_SHIM_PATTERNS = [
|
|
24
|
+
/\bfunction\b/,
|
|
25
|
+
/\breturn\b/,
|
|
26
|
+
/\bif\s*\(/,
|
|
27
|
+
/\bfor\s*\(/,
|
|
28
|
+
/\bwhile\s*\(/,
|
|
29
|
+
/\bconst\s+\w+\s*=/,
|
|
30
|
+
/=>\s*\{/
|
|
8
31
|
];
|
|
32
|
+
/**
|
|
33
|
+
* Behavioural probe of "doctor checks live under a code-driven fixed-registry
|
|
34
|
+
* tree, and each openspec change is generated from a real doctor finding".
|
|
35
|
+
*
|
|
36
|
+
* The previous version listed doctor/audit/openspec files and passed if any of
|
|
37
|
+
* them mentioned "doctor", "audit", "openspec" or "health" — `doctor-service.ts`
|
|
38
|
+
* satisfies that from its own filename.
|
|
39
|
+
*/
|
|
9
40
|
export async function runJ11Contract(ctx) {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
41
|
+
const row = requireBaselineRow(ctx);
|
|
42
|
+
const missing = missingSourceFiles(ctx, row);
|
|
43
|
+
const checksDirAbs = join(ctx.projectRoot, ...CHECKS_DIR);
|
|
44
|
+
const checkFiles = existsSync(checksDirAbs) ? readdirSync(checksDirAbs).filter((f) => f.endsWith('.ts')) : [];
|
|
45
|
+
const pluginIds = PLUGINS.map((p) => p.name);
|
|
46
|
+
const uniquePluginIds = new Set(pluginIds);
|
|
47
|
+
const shimAbs = join(ctx.projectRoot, ...LEGACY_SHIM);
|
|
48
|
+
const shimSource = existsSync(shimAbs) ? readFileSync(shimAbs, 'utf8') : '';
|
|
49
|
+
const shimLines = executableLines(shimSource);
|
|
50
|
+
const shimLogic = shimLines.filter((line) => FORBIDDEN_SHIM_PATTERNS.some((re) => re.test(line)));
|
|
51
|
+
const shimReexports = /export\s+(\*|\{[\s\S]*?\}|type\s*\{[\s\S]*?\})\s*from\s*'/.test(shimSource);
|
|
52
|
+
const shimIsReexportOnly = shimLines.length > 0 && shimLogic.length === 0 && shimReexports;
|
|
53
|
+
const root = mkdtempSync(join(tmpdir(), 'cbl-J11-'));
|
|
54
|
+
try {
|
|
55
|
+
const finding = { id: 'L3:l3-memory-health', rule: 'r', detail: 'd', severity: 'fail' };
|
|
56
|
+
const first = proposeFromDoctor({ projectRoot: root, finding, clock: FROZEN_CLOCK });
|
|
57
|
+
const second = proposeFromDoctor({ projectRoot: root, finding, clock: FROZEN_CLOCK });
|
|
58
|
+
const proposalMentionsFinding = existsSync(first.proposalPath)
|
|
59
|
+
? readFileSync(first.proposalPath, 'utf8').includes(finding.id)
|
|
60
|
+
: false;
|
|
61
|
+
const result = combineProbes([
|
|
62
|
+
probe(missing.length === 0, `baseline sourceFiles present (${row.sourceFiles.length})`),
|
|
63
|
+
probe(checkFiles.length > 0 && PLUGINS.length === checkFiles.length, `the plugin registry enumerates every check module (${String(PLUGINS.length)} plugins / ${String(checkFiles.length)} checks)`),
|
|
64
|
+
probe(uniquePluginIds.size === PLUGINS.length, `plugin ids are unique (${String(uniquePluginIds.size)}/${String(PLUGINS.length)})`),
|
|
65
|
+
probe(shimIsReexportOnly, `the legacy doctor-service.ts shim holds no logic (${String(shimLines.length)} executable lines, ${String(shimLogic.length)} with logic${shimLogic.length > 0 ? `: ${shimLogic.slice(0, 3).join(' | ')}` : ''})`),
|
|
66
|
+
probe(first.changeId === '2020-01-01-fix-l3-l3-memory-health', `the change id is derived from the doctor finding (${first.changeId})`),
|
|
67
|
+
probe(first.created === true, 'the first proposal is created'),
|
|
68
|
+
probe(proposalMentionsFinding, 'the proposal names the originating finding id'),
|
|
69
|
+
probe(second.created === false, 'regenerating from the same finding is idempotent (created=false)')
|
|
70
|
+
]);
|
|
71
|
+
const artifact = row.sourceFiles[0] ?? 'src/services/doctor/doctor-service/index.ts';
|
|
72
|
+
if (result.ok)
|
|
73
|
+
return pass(ctx, artifact);
|
|
74
|
+
return fail(ctx, artifact, 'doctor checks are a code-driven fixed-registry tree and openspec changes come from real findings', result.detail, 'J11 invariant broken: the doctor check registry or the finding-to-change conversion no longer holds');
|
|
19
75
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return src.includes('doctor') || src.includes('audit') || src.includes('openspec') || src.includes('health');
|
|
23
|
-
});
|
|
24
|
-
if (!primary) {
|
|
25
|
-
return {
|
|
26
|
-
journeyId: 'J11',
|
|
27
|
-
contract: 'cli-output-golden',
|
|
28
|
-
status: 'fail',
|
|
29
|
-
diff: { before: 'doctor/audit/openspec references health/audit/openspec', after: 'no such reference', reason: 'J11#1 broken: doctor/audit/openspec services are not wired' },
|
|
30
|
-
artifactPath: existing[0]
|
|
31
|
-
};
|
|
76
|
+
finally {
|
|
77
|
+
rmSync(root, { recursive: true, force: true });
|
|
32
78
|
}
|
|
33
|
-
return {
|
|
34
|
-
journeyId: 'J11',
|
|
35
|
-
contract: 'cli-output-golden',
|
|
36
|
-
status: 'pass',
|
|
37
|
-
artifactPath: existing[0]
|
|
38
|
-
};
|
|
39
79
|
}
|
|
@@ -1,2 +1,14 @@
|
|
|
1
1
|
import type { GuardContext, GuardRunResult } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Behavioural probe of the lease lifecycle.
|
|
4
|
+
*
|
|
5
|
+
* The previous version passed if one of four worktree/dispatch files mentioned
|
|
6
|
+
* both "lease" and "release" — `worktree-lease.ts` satisfies that from its own
|
|
7
|
+
* filename.
|
|
8
|
+
*
|
|
9
|
+
* Here the real pure module is driven: a fresh lease must be active, releasing
|
|
10
|
+
* twice must be a no-op, a duplicate consumption must not append, an in-flight
|
|
11
|
+
* lease must never be gc-eligible, and a persisted lease must survive a
|
|
12
|
+
* serialize/deserialize round trip.
|
|
13
|
+
*/
|
|
2
14
|
export declare function runJ12Contract(ctx: GuardContext): Promise<GuardRunResult>;
|