peaks-loop 4.0.49 → 4.0.51
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 +38 -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/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/hooks-commands.js +55 -38
- package/dist/cli/commands/share-commands.js +113 -20
- 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/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/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-modes.d.ts +13 -2
- package/dist/services/code/auto-compact-modes.js +20 -4
- package/dist/services/code/post-compact-detector.js +20 -11
- package/dist/services/code/step-08-gate.js +21 -6
- package/dist/services/config/config-safety.js +11 -9
- package/dist/services/dispatch/sub-agent-dispatcher.d.ts +11 -30
- package/dist/services/dispatch/sub-agent-dispatcher.js +5 -48
- package/dist/services/final-review/pre-post-diff.js +10 -2
- package/dist/services/ide/adapters/claude-code-adapter.js +0 -1
- package/dist/services/ide/adapters/codex-adapter.js +1 -2
- package/dist/services/ide/adapters/cursor-adapter.js +1 -2
- package/dist/services/ide/adapters/hermes-adapter.js +1 -2
- package/dist/services/ide/adapters/openclaw-adapter.js +1 -2
- package/dist/services/ide/adapters/qoder-adapter.js +1 -2
- package/dist/services/ide/adapters/tongyi-lingma-adapter.js +1 -2
- package/dist/services/ide/adapters/trae-adapter.js +1 -2
- package/dist/services/ide/adapters/zcode-adapter.js +0 -1
- package/dist/services/ide/ide-types.d.ts +0 -2
- package/dist/services/observability/observability-service.d.ts +1 -1
- package/dist/services/scan/api-diff-types.js +20 -2
- package/dist/services/security/safe-settings-path.js +19 -1
- package/dist/services/skill/skill-search-service.d.ts +3 -3
- 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/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/package.json +9 -7
- package/scripts/copy-templates.mjs +0 -12
- package/scripts/install-skills.mjs +154 -53
- package/skills/bee/peaks-perf-audit/SKILL.md +2 -2
- package/skills/bee/peaks-perf-audit/references/audit-protocol.md +1 -1
- package/skills/bee/peaks-prd/SKILL.md +3 -3
- package/skills/bee/peaks-prd/references/prd-for-multi-pass.md +1 -1
- package/skills/bee/peaks-prd/references/workflow.md +1 -1
- package/skills/bee/peaks-qa/SKILL.md +6 -7
- package/skills/bee/peaks-qa/references/external-capability-guidance.md +1 -1
- package/skills/bee/peaks-qa/references/qa-fanout-contract.md +1 -1
- package/skills/bee/peaks-qa/references/reading-handoff-frontmatter.md +2 -2
- package/skills/bee/peaks-rd/SKILL.md +2 -3
- package/skills/bee/peaks-rd/references/code-reviewer-4dim-hint.md +1 -1
- package/skills/bee/peaks-rd/references/external-references.md +1 -1
- package/skills/bee/peaks-rd/references/mandatory-perf-baseline.md +1 -1
- package/skills/bee/peaks-rd/references/ocr-multilang-1.8.md +2 -2
- package/skills/bee/peaks-rd/references/parallel-review-fanout.md +2 -2
- package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +11 -8
- package/skills/bee/peaks-rd/references/rd-runbook.md +1 -1
- package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +7 -7
- package/skills/bee/peaks-rd/references/rd-transition-gates.md +1 -1
- package/skills/bee/peaks-rd/references/reading-v2-slice-results.md +1 -1
- package/skills/bee/peaks-rd/references/v2-12-fanout-collapse.md +7 -5
- package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +3 -3
- package/skills/bee/peaks-reviewer/SKILL.md +1 -1
- package/skills/bee/peaks-security-audit/SKILL.md +3 -3
- package/skills/bee/peaks-security-audit/references/audit-protocol.md +1 -1
- package/skills/bee/peaks-txt/references/context-capsule.md +1 -1
- package/skills/bee/peaks-ui/SKILL.md +1 -1
- package/skills/peaks-audit/SKILL.md +1 -1
- package/skills/peaks-code/SKILL.md +20 -18
- package/skills/peaks-code/references/context-governance.md +1 -1
- package/skills/peaks-code/references/dag-orchestrator.md +3 -4
- package/skills/peaks-code/references/external-references.md +1 -1
- package/skills/peaks-code/references/external-skill-invocation.md +2 -2
- package/skills/peaks-code/references/fanout-mandatory.md +3 -3
- package/skills/peaks-code/references/frontend-only-mode.md +2 -2
- package/skills/peaks-code/references/gstack-integration.md +1 -1
- package/skills/peaks-code/references/micro-cycle.md +1 -1
- package/skills/peaks-code/references/periodic-checkpoint.md +4 -4
- package/skills/peaks-code/references/project-scan-checklist.md +1 -1
- package/skills/peaks-code/references/resume-detection.md +1 -1
- package/skills/peaks-code/references/runbook.md +6 -3
- package/skills/peaks-code/references/session-overload-signal-index.md +6 -4
- package/skills/peaks-code/references/startup-sequence.md +17 -17
- package/skills/peaks-code/references/step-0-8-gate.md +1 -1
- package/skills/peaks-code/references/step-11-memory-sediment.md +2 -2
- package/skills/peaks-code/references/sub-agent-dispatch.md +26 -25
- package/skills/peaks-code/references/swarm-dispatch-contract.md +1 -1
- package/skills/peaks-code/references/workflow-gates-and-types.md +3 -3
- package/skills/peaks-code/references/worktree-governance.md +1 -1
- package/skills/peaks-final-review/SKILL.md +3 -3
- package/skills/peaks-ide/references/audit-log-helper.md +5 -4
- package/skills/peaks-resume/SKILL.md +1 -1
- package/skills/peaks-slice-decompose/SKILL.md +4 -4
- package/skills/peaks-slice-decompose/references/cross-pass-edge-interpretation.md +1 -1
- package/skills/peaks-slice-decompose/references/granularity-decision.md +1 -1
- package/skills/peaks-slice-decompose/references/v2-schema.md +2 -2
- package/skills/peaks-solo/SKILL.md +1 -2
- 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,61 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { classifyResume } from '../../skill/resume-detector.js';
|
|
5
|
+
import { combineProbes, fail, missingSourceFiles, pass, probe, requireBaselineRow } from './_shared.js';
|
|
6
|
+
const SESSION_ID = '2026-09-15-guard-j06';
|
|
7
|
+
function write(sessionDir, relative, body) {
|
|
8
|
+
const abs = join(sessionDir, relative);
|
|
9
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
10
|
+
writeFileSync(abs, body);
|
|
11
|
+
}
|
|
12
|
+
function request(body) {
|
|
13
|
+
return `# request\n\n## Status\n\n- state: ${body}\n`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Behavioural probe for "the resume option always identifies the DEEPEST
|
|
17
|
+
* completed gate".
|
|
18
|
+
*
|
|
19
|
+
* The load-bearing part is that the SAME fixture is walked from shallow to
|
|
20
|
+
* deep: with only a PRD request the classifier must stop at `rd-planning`;
|
|
21
|
+
* the moment a deeper RD `qa-handoff` request appears (with its review
|
|
22
|
+
* artifacts) the point must move to `qa-validation`; adding the txt handoff
|
|
23
|
+
* must end the workflow. A classifier that returned a constant point, or that
|
|
24
|
+
* looked at file existence alone, cannot track that progression.
|
|
25
|
+
*
|
|
26
|
+
* The old check asserted that a file named `session-resume-service.ts` exists
|
|
27
|
+
* and contains the word "resume" — which the filename already guarantees.
|
|
28
|
+
*/
|
|
12
29
|
export async function runJ06Contract(ctx) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
const row = requireBaselineRow(ctx);
|
|
31
|
+
const missing = missingSourceFiles(ctx, row);
|
|
32
|
+
const root = mkdtempSync(join(tmpdir(), 'cbl-J06-'));
|
|
33
|
+
const peaksRoot = join(root, '_runtime');
|
|
34
|
+
const sessionDir = join(peaksRoot, SESSION_ID);
|
|
35
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
36
|
+
try {
|
|
37
|
+
write(sessionDir, 'prd/requests/001-shallow.md', request('handed-off'));
|
|
38
|
+
const shallow = classifyResume(SESSION_ID, peaksRoot);
|
|
39
|
+
// `ridOf()` strips the `NNN-` index prefix, so the review artifacts are
|
|
40
|
+
// keyed by `deeper`, not `001-deeper`.
|
|
41
|
+
write(sessionDir, 'rd/requests/001-deeper.md', request('qa-handoff'));
|
|
42
|
+
write(sessionDir, 'rd/code-review-deeper.md', '# code review\n');
|
|
43
|
+
write(sessionDir, 'audit/security-deeper.md', '# security\n');
|
|
44
|
+
const deeper = classifyResume(SESSION_ID, peaksRoot);
|
|
45
|
+
write(sessionDir, 'txt/handoff.md', '# handoff\n');
|
|
46
|
+
const done = classifyResume(SESSION_ID, peaksRoot);
|
|
47
|
+
const result = combineProbes([
|
|
48
|
+
probe(missing.length === 0, `baseline sourceFiles present (${row.sourceFiles.length})`),
|
|
49
|
+
probe(shallow.kind === 'resume' && shallow.point === 'rd-planning', `shallow fixture resumes at rd-planning (saw kind=${shallow.kind} point=${String(shallow.point)})`),
|
|
50
|
+
probe(deeper.kind === 'resume' && deeper.point === 'qa-validation', `adding an RD qa-handoff request moves the point DEEPER to qa-validation (saw kind=${deeper.kind} point=${String(deeper.point)})`),
|
|
51
|
+
probe(done.kind === 'complete', `adding txt/handoff.md marks the workflow complete (saw kind=${done.kind} point=${String(done.point)})`)
|
|
52
|
+
]);
|
|
53
|
+
const artifact = row.sourceFiles[0] ?? 'src/services/skill/resume-detector.ts';
|
|
54
|
+
if (result.ok)
|
|
55
|
+
return pass(ctx, artifact);
|
|
56
|
+
return fail(ctx, artifact, 'resume identifies the deepest completed gate (rd-planning → qa-validation → complete)', result.detail, 'J06 invariant broken: resume no longer tracks the deepest completed gate');
|
|
22
57
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const src = readFileSync(join(ctx.projectRoot, f), 'utf8');
|
|
26
|
-
return src.toLowerCase().includes('resume');
|
|
27
|
-
});
|
|
28
|
-
if (!mentionsResume) {
|
|
29
|
-
return {
|
|
30
|
-
journeyId: 'J06',
|
|
31
|
-
contract: 'workflow-trace',
|
|
32
|
-
status: 'fail',
|
|
33
|
-
diff: { before: 'resume service files mention "resume"', after: 'none mention resume', reason: 'J06#1 broken: resume service files do not reference the resume concept' },
|
|
34
|
-
artifactPath: existing[0]
|
|
35
|
-
};
|
|
58
|
+
finally {
|
|
59
|
+
rmSync(root, { recursive: true, force: true });
|
|
36
60
|
}
|
|
37
|
-
return {
|
|
38
|
-
journeyId: 'J06',
|
|
39
|
-
contract: 'workflow-trace',
|
|
40
|
-
status: 'pass',
|
|
41
|
-
artifactPath: existing[0]
|
|
42
|
-
};
|
|
43
61
|
}
|
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
import type { GuardContext, GuardRunResult } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Behavioural probe of the (mtime, sha256) gate.
|
|
4
|
+
*
|
|
5
|
+
* A cache entry is written for a passing test, then the file's CONTENT is
|
|
6
|
+
* changed while its mtime is restored to the original value. If the gate ever
|
|
7
|
+
* degraded to an mtime-only (or filename-only) check, the cache would report a
|
|
8
|
+
* hit for a file whose bytes changed — exactly the fake-green the invariant
|
|
9
|
+
* forbids. The mtime-only and status gates are probed the same way.
|
|
10
|
+
*/
|
|
2
11
|
export declare function runJ07Contract(ctx: GuardContext): Promise<GuardRunResult>;
|
|
@@ -1,54 +1,83 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { 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
|
-
'src/cli/commands/test-commands.ts',
|
|
11
|
-
'src/services/test/',
|
|
12
|
-
'src/services/test-runner/'
|
|
13
|
-
];
|
|
14
|
-
export async function runJ07Contract(ctx) {
|
|
15
|
-
// 1. CLI probe: `peaks test --help` must produce non-empty output.
|
|
16
|
-
let helpOk = false;
|
|
4
|
+
import { TEST_CACHE_DIR, isCacheable, mtimeOfFile, readTestCache, recordTestResult, sha256OfFile, testCacheDir, writeTestCache } from '../../test-cache/test-cache-service.js';
|
|
5
|
+
import { combineProbes, fail, missingSourceFiles, pass, probe, requireBaselineRow } from './_shared.js';
|
|
6
|
+
const TEST_NAME = 'fingerprint probe';
|
|
7
|
+
/** Highest vitest major the frozen baseline allows ("MUST NOT be locked at 5.x"). */
|
|
8
|
+
const MAX_VITEST_MAJOR = 4;
|
|
9
|
+
function vitestMajor(projectRoot) {
|
|
17
10
|
try {
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}).toString('utf8');
|
|
25
|
-
helpOk = out.length > 0 && !out.includes('unknown command');
|
|
11
|
+
const pkg = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8'));
|
|
12
|
+
const raw = pkg.devDependencies?.['vitest'] ?? pkg.dependencies?.['vitest'];
|
|
13
|
+
if (typeof raw !== 'string')
|
|
14
|
+
return null;
|
|
15
|
+
const major = /(\d+)/.exec(raw);
|
|
16
|
+
return major === null ? null : Number(major[1]);
|
|
26
17
|
}
|
|
27
18
|
catch {
|
|
28
|
-
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Behavioural probe of the (mtime, sha256) gate.
|
|
24
|
+
*
|
|
25
|
+
* A cache entry is written for a passing test, then the file's CONTENT is
|
|
26
|
+
* changed while its mtime is restored to the original value. If the gate ever
|
|
27
|
+
* degraded to an mtime-only (or filename-only) check, the cache would report a
|
|
28
|
+
* hit for a file whose bytes changed — exactly the fake-green the invariant
|
|
29
|
+
* forbids. The mtime-only and status gates are probed the same way.
|
|
30
|
+
*/
|
|
31
|
+
export async function runJ07Contract(ctx) {
|
|
32
|
+
const row = requireBaselineRow(ctx);
|
|
33
|
+
const missing = missingSourceFiles(ctx, row);
|
|
34
|
+
const root = mkdtempSync(join(tmpdir(), 'cbl-J07-'));
|
|
35
|
+
const file = join(root, 'probe.test.ts');
|
|
36
|
+
try {
|
|
37
|
+
writeFileSync(file, 'export const a = 1;\n');
|
|
38
|
+
recordTestResult(root, file, 'vitest', {
|
|
39
|
+
testName: TEST_NAME,
|
|
40
|
+
status: 'passed',
|
|
41
|
+
durationMs: 1,
|
|
42
|
+
lastRun: new Date().toISOString()
|
|
43
|
+
});
|
|
44
|
+
const baseline = isCacheable(root, file, TEST_NAME);
|
|
45
|
+
const cached = readTestCache(root, file);
|
|
46
|
+
// Gate 1, isolated: the file's bytes change but the recorded mtime is
|
|
47
|
+
// up-to-date, so ONLY the sha check can catch it. If the gate degraded to
|
|
48
|
+
// an mtime-only check this reports a hit for a file nobody verified.
|
|
49
|
+
writeFileSync(file, 'export const a = 2;\n');
|
|
50
|
+
writeTestCache(root, { ...cached, fileMtime: mtimeOfFile(file) });
|
|
51
|
+
const afterSilentEdit = isCacheable(root, file, TEST_NAME);
|
|
52
|
+
// Gate 2, isolated: the sha is up-to-date but the recorded mtime is stale.
|
|
53
|
+
writeTestCache(root, { ...cached, fileSha256: sha256OfFile(file), fileMtime: cached.fileMtime - 5_000 });
|
|
54
|
+
const afterTouch = isCacheable(root, file, TEST_NAME);
|
|
55
|
+
// A non-passing status must never be served as a hit.
|
|
56
|
+
recordTestResult(root, file, 'vitest', {
|
|
57
|
+
testName: 'skipped probe',
|
|
58
|
+
status: 'skipped',
|
|
59
|
+
durationMs: 0,
|
|
60
|
+
lastRun: new Date().toISOString()
|
|
61
|
+
});
|
|
62
|
+
const skipped = isCacheable(root, file, 'skipped probe');
|
|
63
|
+
const unknown = isCacheable(root, file, 'never recorded');
|
|
64
|
+
const major = vitestMajor(ctx.projectRoot);
|
|
65
|
+
const result = combineProbes([
|
|
66
|
+
probe(missing.length === 0, `baseline sourceFiles present (${row.sourceFiles.length})`),
|
|
67
|
+
probe(baseline.hit, `an unchanged passing test is a cache hit (reason=${String(baseline.reason)})`),
|
|
68
|
+
probe(!afterSilentEdit.hit && afterSilentEdit.reason === 'sha-changed', `a same-mtime content edit is NOT a hit (hit=${String(afterSilentEdit.hit)} reason=${String(afterSilentEdit.reason)})`),
|
|
69
|
+
probe(!afterTouch.hit && afterTouch.reason === 'mtime-changed', `a touched file is NOT a hit (hit=${String(afterTouch.hit)} reason=${String(afterTouch.reason)})`),
|
|
70
|
+
probe(!skipped.hit && skipped.reason === 'previous-skipped', `a previously skipped test is NOT a hit (hit=${String(skipped.hit)} reason=${String(skipped.reason)})`),
|
|
71
|
+
probe(!unknown.hit && unknown.reason === 'no-cache', `an unrecorded test is NOT a hit (reason=${String(unknown.reason)})`),
|
|
72
|
+
probe(testCacheDir(ctx.projectRoot).replace(/\\/g, '/').endsWith(`.peaks/_runtime/${TEST_CACHE_DIR}`), `the cache writes under .peaks/_runtime/${TEST_CACHE_DIR} (got ${testCacheDir(ctx.projectRoot)})`),
|
|
73
|
+
probe(major !== null && major <= MAX_VITEST_MAJOR, `vitest major is <= ${String(MAX_VITEST_MAJOR)} (saw ${String(major)})`)
|
|
74
|
+
]);
|
|
75
|
+
const artifact = row.sourceFiles[1] ?? 'src/services/test-cache/test-cache-service.ts';
|
|
76
|
+
if (result.ok)
|
|
77
|
+
return pass(ctx, artifact);
|
|
78
|
+
return fail(ctx, artifact, 'the per-test fingerprint cache only serves a hit for a file whose mtime AND sha256 are unchanged and whose last status passed', result.detail, 'J07 invariant broken: the fingerprint cache can return passed for an unverified file');
|
|
29
79
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const abs = join(ctx.projectRoot, f);
|
|
33
|
-
if (!existsSync(abs))
|
|
34
|
-
return false;
|
|
35
|
-
if (existsSync(abs) && readFileSync(abs, 'utf8').includes('vitest'))
|
|
36
|
-
return true;
|
|
37
|
-
return false;
|
|
38
|
-
});
|
|
39
|
-
// 3. package.json probe: vitest is in dependencies.
|
|
40
|
-
const pkgPath = join(ctx.projectRoot, 'package.json');
|
|
41
|
-
let pkgHasVitest = false;
|
|
42
|
-
if (existsSync(pkgPath)) {
|
|
43
|
-
const pkg = readFileSync(pkgPath, 'utf8');
|
|
44
|
-
pkgHasVitest = /"vitest"\s*:/i.test(pkg);
|
|
80
|
+
finally {
|
|
81
|
+
rmSync(root, { recursive: true, force: true });
|
|
45
82
|
}
|
|
46
|
-
const ok = helpOk && sourceMentionsVitest && pkgHasVitest;
|
|
47
|
-
return {
|
|
48
|
-
journeyId: 'J07',
|
|
49
|
-
contract: 'cli-output-golden',
|
|
50
|
-
status: ok ? 'pass' : 'fail',
|
|
51
|
-
...(ok ? {} : { diff: { before: 'peaks test --help works, test commands reference vitest, package.json has vitest', after: `helpOk=${helpOk} sourceMentionsVitest=${sourceMentionsVitest} pkgHasVitest=${pkgHasVitest}`, reason: 'J07#1 broken: peaks test does not delegate to vitest' } }),
|
|
52
|
-
artifactPath: 'src/cli/commands/test-commands.ts'
|
|
53
|
-
};
|
|
54
83
|
}
|
|
@@ -1,2 +1,13 @@
|
|
|
1
1
|
import type { GuardContext, GuardRunResult } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Behavioural probe of the crystallization gate.
|
|
4
|
+
*
|
|
5
|
+
* Before this the contract only asserted that one of three candidate files
|
|
6
|
+
* exists and mentions "crystalliz"/"sediment"/"promot" — the file it found is
|
|
7
|
+
* named `crystallization-service.ts`, so the check restated its own filename.
|
|
8
|
+
*
|
|
9
|
+
* Here the real gate is driven with a completed and an incomplete task state:
|
|
10
|
+
* a task whose `gates_passed` is false must be refused at the Zod boundary AND
|
|
11
|
+
* again at the service boundary.
|
|
12
|
+
*/
|
|
2
13
|
export declare function runJ08Contract(ctx: GuardContext): Promise<GuardRunResult>;
|
|
@@ -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>;
|