@planu/cli 4.11.9 → 4.12.2
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 +129 -22
- package/dist/engine/abortable-process-runner.d.ts +8 -0
- package/dist/engine/abortable-process-runner.js +107 -0
- package/dist/engine/cascade-hooks/core/append-releases.d.ts +1 -0
- package/dist/engine/cascade-hooks/core/append-releases.js +51 -14
- package/dist/engine/cascade-hooks/hooks/orchestration-plan.hook.js +16 -21
- package/dist/engine/ci-generator/yaml-builder.js +5 -1
- package/dist/engine/context-md-generator.js +3 -1
- package/dist/engine/criterion-identity.d.ts +6 -0
- package/dist/engine/criterion-identity.js +63 -0
- package/dist/engine/evidence-gates/lifecycle-gate.js +22 -44
- package/dist/engine/evidence-index/index-builder.js +7 -19
- package/dist/engine/frontmatter-parser.d.ts +5 -0
- package/dist/engine/frontmatter-parser.js +24 -0
- package/dist/engine/project-command-runtime.d.ts +5 -0
- package/dist/{tools/validate-runtime.js → engine/project-command-runtime.js} +8 -21
- package/dist/engine/project-graph/builder.js +1 -35
- package/dist/engine/project-graph/cache.js +37 -1
- package/dist/engine/qa-gate.d.ts +4 -1
- package/dist/engine/qa-gate.js +72 -59
- package/dist/engine/safety/cross-process-lock.d.ts +1 -6
- package/dist/engine/safety/cross-process-lock.js +18 -16
- package/dist/engine/safety/lock-scavenger.d.ts +9 -0
- package/dist/engine/safety/lock-scavenger.js +114 -0
- package/dist/engine/session-context-generator.js +24 -2
- package/dist/engine/session-safeguard/learnings-buffer.js +7 -2
- package/dist/engine/session-state/writer.js +15 -8
- package/dist/engine/skill-registry/installer.js +1 -1
- package/dist/engine/spec-format/acceptance-criteria.js +8 -26
- package/dist/engine/spec-grounding/contract.d.ts +3 -1
- package/dist/engine/spec-grounding/contract.js +39 -14
- package/dist/engine/universal-rules/installer.js +2 -2
- package/dist/engine/validation-evidence-digest.d.ts +4 -0
- package/dist/engine/validation-evidence-digest.js +83 -0
- package/dist/engine/validation-evidence-ledger.d.ts +30 -0
- package/dist/engine/validation-evidence-ledger.js +274 -0
- package/dist/engine/validation-impact-planner.d.ts +4 -0
- package/dist/engine/validation-impact-planner.js +71 -0
- package/dist/engine/validator/spec-compliance-runner.d.ts +1 -1
- package/dist/engine/validator/spec-compliance-runner.js +37 -40
- package/dist/engine/validator/validation-report-writer.d.ts +1 -0
- package/dist/engine/validator/validation-report-writer.js +1 -1
- package/dist/engine/validator.js +7 -11
- package/dist/storage/convention-baseline.js +5 -0
- package/dist/storage/session-state-store.js +13 -6
- package/dist/storage/spec-store.d.ts +5 -0
- package/dist/storage/spec-store.js +7 -0
- package/dist/tools/git/git-helpers.d.ts +5 -1
- package/dist/tools/git/git-helpers.js +3 -2
- package/dist/tools/init-project/conventions-writer.js +1 -1
- package/dist/tools/init-project/scaffold-writer.js +1 -1
- package/dist/tools/safe-handler.d.ts +5 -1
- package/dist/tools/safe-handler.js +73 -12
- package/dist/tools/status-handler.d.ts +1 -1
- package/dist/tools/status-handler.js +27 -66
- package/dist/tools/update-status/dod-gates.js +7 -31
- package/dist/tools/update-status/evidence-gate.js +2 -4
- package/dist/tools/update-status/force-done-audit.d.ts +2 -0
- package/dist/tools/update-status/force-done-audit.js +23 -0
- package/dist/tools/update-status/qa-gate.d.ts +1 -0
- package/dist/tools/update-status/qa-gate.js +7 -51
- package/dist/tools/update-status-convention-gate.js +132 -97
- package/dist/tools/validate-lint.d.ts +1 -1
- package/dist/tools/validate-lint.js +78 -165
- package/dist/tools/validate.js +3 -1
- package/dist/transports/http-transport.js +22 -8
- package/dist/transports/mcp-types.d.ts +2 -2
- package/dist/transports/transport-factory.js +110 -12
- package/dist/types/abortable-process.d.ts +26 -0
- package/dist/types/abortable-process.js +2 -0
- package/dist/types/criterion-identity.d.ts +12 -0
- package/dist/types/criterion-identity.js +2 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/types/qa-gate.d.ts +0 -5
- package/dist/types/spec-format.d.ts +2 -0
- package/dist/types/spec-lock-v2.d.ts +7 -0
- package/dist/types/validation-evidence.d.ts +62 -0
- package/dist/types/validation-evidence.js +2 -0
- package/package.json +43 -23
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
- package/dist/storage/qa-gate-store.d.ts +0 -4
- package/dist/storage/qa-gate-store.js +0 -24
- package/dist/tools/validate-runtime.d.ts +0 -14
- package/dist/types/data/estimation.d.ts +0 -147
- package/dist/types/data/estimation.js +0 -2
- package/dist/types/data/index.d.ts +0 -5
- package/dist/types/data/index.js +0 -6
- package/dist/types/data/velocity.d.ts +0 -168
- package/dist/types/data/velocity.js +0 -4
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CrossProcessLockMetadata, LockScavengeMetrics } from '../../types/spec-lock-v2.js';
|
|
2
|
+
export declare function getLockScavengeMetrics(): Readonly<LockScavengeMetrics>;
|
|
3
|
+
export declare function resetLockScavengeMetricsForTests(): void;
|
|
4
|
+
export declare function readValidatedLockFile(path: string): Promise<{
|
|
5
|
+
raw: string;
|
|
6
|
+
metadata: CrossProcessLockMetadata;
|
|
7
|
+
} | null>;
|
|
8
|
+
export declare function scavengeStaleLockArtifacts(projectPath: string, onlySpecId?: string): Promise<LockScavengeMetrics>;
|
|
9
|
+
//# sourceMappingURL=lock-scavenger.d.ts.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { readFile, readdir, unlink } from 'node:fs/promises';
|
|
2
|
+
import { hostname as osHostname } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { enrichWithStaleness } from './lock-staleness.js';
|
|
5
|
+
const MIN_TTL_MS = 60_000;
|
|
6
|
+
const MAX_TTL_MS = 86_400_000;
|
|
7
|
+
const metrics = {
|
|
8
|
+
scavengedLocks: 0,
|
|
9
|
+
scavengedBackups: 0,
|
|
10
|
+
preservedLive: 0,
|
|
11
|
+
preservedCorrupt: 0,
|
|
12
|
+
};
|
|
13
|
+
export function getLockScavengeMetrics() {
|
|
14
|
+
return { ...metrics };
|
|
15
|
+
}
|
|
16
|
+
export function resetLockScavengeMetricsForTests() {
|
|
17
|
+
metrics.scavengedLocks = 0;
|
|
18
|
+
metrics.scavengedBackups = 0;
|
|
19
|
+
metrics.preservedLive = 0;
|
|
20
|
+
metrics.preservedCorrupt = 0;
|
|
21
|
+
}
|
|
22
|
+
function locksDir(projectPath) {
|
|
23
|
+
return join(projectPath, 'data', '.locks', 'planu');
|
|
24
|
+
}
|
|
25
|
+
function isValidLockMetadata(value) {
|
|
26
|
+
if (typeof value !== 'object' || value === null) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const lock = value;
|
|
30
|
+
return (typeof lock.specId === 'string' &&
|
|
31
|
+
lock.specId.trim().length > 0 &&
|
|
32
|
+
typeof lock.sessionId === 'string' &&
|
|
33
|
+
lock.sessionId.trim().length > 0 &&
|
|
34
|
+
typeof lock.hostname === 'string' &&
|
|
35
|
+
lock.hostname.trim().length > 0 &&
|
|
36
|
+
typeof lock.pid === 'number' &&
|
|
37
|
+
Number.isSafeInteger(lock.pid) &&
|
|
38
|
+
lock.pid > 0 &&
|
|
39
|
+
typeof lock.ttlMs === 'number' &&
|
|
40
|
+
Number.isSafeInteger(lock.ttlMs) &&
|
|
41
|
+
lock.ttlMs >= MIN_TTL_MS &&
|
|
42
|
+
lock.ttlMs <= MAX_TTL_MS &&
|
|
43
|
+
typeof lock.acquiredAt === 'string' &&
|
|
44
|
+
!Number.isNaN(Date.parse(lock.acquiredAt)) &&
|
|
45
|
+
typeof lock.lastHeartbeatAt === 'string' &&
|
|
46
|
+
!Number.isNaN(Date.parse(lock.lastHeartbeatAt)));
|
|
47
|
+
}
|
|
48
|
+
export async function readValidatedLockFile(path) {
|
|
49
|
+
try {
|
|
50
|
+
const raw = await readFile(path, 'utf-8');
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
return isValidLockMetadata(parsed) ? { raw, metadata: parsed } : null;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function scavengeStaleLockArtifacts(projectPath, onlySpecId) {
|
|
59
|
+
const dir = locksDir(projectPath);
|
|
60
|
+
let entries;
|
|
61
|
+
try {
|
|
62
|
+
entries = await readdir(dir);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return { scavengedLocks: 0, scavengedBackups: 0, preservedLive: 0, preservedCorrupt: 0 };
|
|
66
|
+
}
|
|
67
|
+
const delta = {
|
|
68
|
+
scavengedLocks: 0,
|
|
69
|
+
scavengedBackups: 0,
|
|
70
|
+
preservedLive: 0,
|
|
71
|
+
preservedCorrupt: 0,
|
|
72
|
+
};
|
|
73
|
+
const candidates = entries.filter((entry) => {
|
|
74
|
+
const match = /^(.*\.lock)(?:\.bak\.\d+)?$/.exec(entry);
|
|
75
|
+
return match !== null && (!onlySpecId || match[1] === `${onlySpecId}.lock`);
|
|
76
|
+
});
|
|
77
|
+
for (const entry of candidates) {
|
|
78
|
+
const path = join(dir, entry);
|
|
79
|
+
const first = await readValidatedLockFile(path);
|
|
80
|
+
if (!first) {
|
|
81
|
+
delta.preservedCorrupt += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const info = enrichWithStaleness(first.metadata, osHostname(), Date.now());
|
|
85
|
+
if (!info.stale) {
|
|
86
|
+
delta.preservedLive += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const current = await readValidatedLockFile(path);
|
|
90
|
+
if (current?.raw !== first.raw) {
|
|
91
|
+
delta.preservedLive += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
await unlink(path);
|
|
96
|
+
if (entry.includes('.lock.bak.')) {
|
|
97
|
+
delta.scavengedBackups += 1;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
delta.scavengedLocks += 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) {
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const key of Object.keys(delta)) {
|
|
110
|
+
metrics[key] += delta[key];
|
|
111
|
+
}
|
|
112
|
+
return delta;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=lock-scavenger.js.map
|
|
@@ -44,6 +44,13 @@ function getPendingApproved(specs) {
|
|
|
44
44
|
.filter((s) => s.status === 'approved')
|
|
45
45
|
.sort((a, b) => b.estimation.devHours - a.estimation.devHours);
|
|
46
46
|
}
|
|
47
|
+
/** Returns active in-progress specs ordered by most recently updated first. */
|
|
48
|
+
function getActiveSpecs(specs) {
|
|
49
|
+
return specs
|
|
50
|
+
.filter((s) => s.status === 'implementing' || s.status === 'review')
|
|
51
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
|
52
|
+
.slice(0, 5);
|
|
53
|
+
}
|
|
47
54
|
/** Fetches the last 3 lessons for the project via lessonsStore (best-effort). */
|
|
48
55
|
async function fetchRecentLessons(projectPath) {
|
|
49
56
|
try {
|
|
@@ -70,6 +77,13 @@ function buildApprovedRows(approved) {
|
|
|
70
77
|
.map((s) => `| ${s.id} | ${s.title} | ${s.estimation.devHours.toFixed(1)}h |`)
|
|
71
78
|
.join('\n');
|
|
72
79
|
}
|
|
80
|
+
/** Builds the markdown rows for the "Active Work" table. */
|
|
81
|
+
function buildActiveRows(active) {
|
|
82
|
+
if (active.length === 0) {
|
|
83
|
+
return '| — | No active specs | — |';
|
|
84
|
+
}
|
|
85
|
+
return active.map((s) => `| ${s.id} | ${s.title} | ${s.status} |`).join('\n');
|
|
86
|
+
}
|
|
73
87
|
/** Renders the Key Lessons section. */
|
|
74
88
|
function buildLessonsSection(lessons) {
|
|
75
89
|
if (lessons.length === 0) {
|
|
@@ -79,7 +93,7 @@ function buildLessonsSection(lessons) {
|
|
|
79
93
|
}
|
|
80
94
|
/** Generates the full session-context.md content. */
|
|
81
95
|
function buildMarkdown(params) {
|
|
82
|
-
const { version, doneCount, approvedCount, doneRows, approvedRows, lessonsContent, generatedAt } = params;
|
|
96
|
+
const { version, activeCount, doneCount, approvedCount, activeRows, doneRows, approvedRows, lessonsContent, generatedAt, } = params;
|
|
83
97
|
return [
|
|
84
98
|
'# Planu — Session Context',
|
|
85
99
|
'',
|
|
@@ -88,7 +102,12 @@ function buildMarkdown(params) {
|
|
|
88
102
|
'',
|
|
89
103
|
'## Current State',
|
|
90
104
|
`- **Version**: ${version}`,
|
|
91
|
-
`- **Status**: ${String(doneCount)}
|
|
105
|
+
`- **Status**: ${String(activeCount)} active / ${String(doneCount)} done / ${String(approvedCount)} approved pending`,
|
|
106
|
+
'',
|
|
107
|
+
'## Active Work',
|
|
108
|
+
'| ID | Title | Status |',
|
|
109
|
+
'|----|-------|--------|',
|
|
110
|
+
activeRows,
|
|
92
111
|
'',
|
|
93
112
|
'## Recently Completed (last 5)',
|
|
94
113
|
'| ID | Title | Completed |',
|
|
@@ -118,12 +137,15 @@ export async function generateSessionContext(projectPath, projectId) {
|
|
|
118
137
|
specStore.listSpecs(projectId),
|
|
119
138
|
fetchRecentLessons(projectPath),
|
|
120
139
|
]);
|
|
140
|
+
const active = getActiveSpecs(allSpecs);
|
|
121
141
|
const done = getRecentlyDone(allSpecs);
|
|
122
142
|
const approved = getPendingApproved(allSpecs);
|
|
123
143
|
const content = buildMarkdown({
|
|
124
144
|
version,
|
|
145
|
+
activeCount: active.length,
|
|
125
146
|
doneCount: allSpecs.filter((s) => s.status === 'done').length,
|
|
126
147
|
approvedCount: approved.length,
|
|
148
|
+
activeRows: buildActiveRows(active),
|
|
127
149
|
doneRows: buildDoneRows(done),
|
|
128
150
|
approvedRows: buildApprovedRows(approved),
|
|
129
151
|
lessonsContent: buildLessonsSection(lessons),
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
// engine/session-safeguard/learnings-buffer.ts — SPEC-585: Append/flush learnings buffer
|
|
2
2
|
import { open, readFile, writeFile, mkdir, unlink } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
5
|
import { hashProjectPath, projectDataDir } from '../../storage/base-store.js';
|
|
5
6
|
const BUFFER_VERSION = 1;
|
|
6
|
-
const MEMORY_MD_PATH = '/Users/kevin/.claude/projects/-Users-kevin-Documents-desarrollo-sdd-mcp-server/memory/MEMORY.md';
|
|
7
7
|
const MAX_RETRIES = 50;
|
|
8
8
|
const JITTER_MS = 100;
|
|
9
9
|
function bufferPath(projectPath) {
|
|
10
10
|
const hash = hashProjectPath(projectPath);
|
|
11
11
|
return join(projectDataDir(hash), 'learnings-buffer.json');
|
|
12
12
|
}
|
|
13
|
+
function claudeMemoryPath(projectPath) {
|
|
14
|
+
const projectKey = projectPath.replaceAll('\\', '-').replaceAll('/', '-');
|
|
15
|
+
return join(homedir(), '.claude', 'projects', projectKey, 'memory', 'MEMORY.md');
|
|
16
|
+
}
|
|
13
17
|
function emptyBuffer() {
|
|
14
18
|
return { version: BUFFER_VERSION, entries: [] };
|
|
15
19
|
}
|
|
@@ -92,7 +96,7 @@ export async function flushBuffer(projectPath) {
|
|
|
92
96
|
if (buffer.entries.length === 0) {
|
|
93
97
|
return 0;
|
|
94
98
|
}
|
|
95
|
-
const memoryPath =
|
|
99
|
+
const memoryPath = claudeMemoryPath(projectPath);
|
|
96
100
|
let existing = '';
|
|
97
101
|
try {
|
|
98
102
|
existing = await readFile(memoryPath, 'utf-8');
|
|
@@ -102,6 +106,7 @@ export async function flushBuffer(projectPath) {
|
|
|
102
106
|
}
|
|
103
107
|
const bullets = buffer.entries.map(entryToMarkdown).join('\n');
|
|
104
108
|
const section = `\n\n## Session Learnings (auto-flushed ${new Date().toISOString().slice(0, 10)})\n\n${bullets}\n`;
|
|
109
|
+
await mkdir(join(memoryPath, '..'), { recursive: true });
|
|
105
110
|
await writeFile(memoryPath, existing + section, 'utf-8');
|
|
106
111
|
await writeBuffer(projectPath, emptyBuffer());
|
|
107
112
|
return buffer.entries.length;
|
|
@@ -123,6 +123,14 @@ export function computeEpicProgress(specs) {
|
|
|
123
123
|
}
|
|
124
124
|
return progress;
|
|
125
125
|
}
|
|
126
|
+
function reconcileActiveSpecIds(existing, specs) {
|
|
127
|
+
const activeFromSpecs = specs
|
|
128
|
+
.filter((spec) => spec.status === 'implementing' || spec.status === 'review')
|
|
129
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
|
130
|
+
.map((spec) => spec.id);
|
|
131
|
+
const knownActive = new Set(activeFromSpecs);
|
|
132
|
+
return [...new Set(existing.filter((specId) => knownActive.has(specId)).concat(activeFromSpecs))];
|
|
133
|
+
}
|
|
126
134
|
/**
|
|
127
135
|
* SPEC-656: Update epicProgress in root session.json after any status change.
|
|
128
136
|
* Best-effort — never throws.
|
|
@@ -130,9 +138,6 @@ export function computeEpicProgress(specs) {
|
|
|
130
138
|
export async function updateEpicProgressInSession(projectPath, specs) {
|
|
131
139
|
try {
|
|
132
140
|
const epicProgress = computeEpicProgress(specs);
|
|
133
|
-
if (Object.keys(epicProgress).length === 0) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
141
|
const filePath = rootFile(projectPath);
|
|
137
142
|
let existingContent = null;
|
|
138
143
|
let index = { activeSpecs: [], updatedAt: '' };
|
|
@@ -143,11 +148,13 @@ export async function updateEpicProgressInSession(projectPath, specs) {
|
|
|
143
148
|
catch {
|
|
144
149
|
/* start fresh */
|
|
145
150
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
+
index.activeSpecs = reconcileActiveSpecIds(index.activeSpecs, specs);
|
|
152
|
+
if (Object.keys(epicProgress).length === 0) {
|
|
153
|
+
delete index.epicProgress;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
index.epicProgress = epicProgress;
|
|
157
|
+
}
|
|
151
158
|
index.updatedAt = new Date().toISOString();
|
|
152
159
|
await stableWriteGeneratedFile(filePath, stableJsonStringify(index), {
|
|
153
160
|
equivalence: stripRootUpdatedAt,
|
|
@@ -195,7 +195,7 @@ export async function installSkill(skillName, source, projectPath, options = {})
|
|
|
195
195
|
source,
|
|
196
196
|
version,
|
|
197
197
|
installedAt: new Date().toISOString(),
|
|
198
|
-
path: installDir,
|
|
198
|
+
path: path.relative(projectPath, installDir).split(path.sep).join('/'),
|
|
199
199
|
securityScan,
|
|
200
200
|
};
|
|
201
201
|
await writeSkillManifest(projectPath, upsertManifestEntry(manifest, entry));
|
|
@@ -1,21 +1,7 @@
|
|
|
1
1
|
import { parseFrontmatterScenarios } from '../validator/spec-compliance-runner.js';
|
|
2
2
|
import { stripFrontmatter } from '../frontmatter-parser.js';
|
|
3
3
|
import { extractListItems, extractSection } from '../validator/extractors.js';
|
|
4
|
-
|
|
5
|
-
return value
|
|
6
|
-
.replace(/^-\s*\[[ x]\]\s*/i, '')
|
|
7
|
-
.replace(/[`*_()[\]:]/g, ' ')
|
|
8
|
-
.replace(/\s+/g, ' ')
|
|
9
|
-
.trim()
|
|
10
|
-
.toLowerCase();
|
|
11
|
-
}
|
|
12
|
-
function stripCriterionIdPrefix(value) {
|
|
13
|
-
return value.replace(/^(AC\d+)\s*[:.)-]?\s*/i, '').trim();
|
|
14
|
-
}
|
|
15
|
-
function detectCriterionId(value, index) {
|
|
16
|
-
const explicit = /^(AC\d+)\b/i.exec(value)?.[1];
|
|
17
|
-
return (explicit ?? `AC${String(index + 1)}`).toUpperCase();
|
|
18
|
-
}
|
|
4
|
+
import { createCriterionIdentity, normalizeCriterionText } from '../criterion-identity.js';
|
|
19
5
|
function extractSectionCriteria(section) {
|
|
20
6
|
const listItems = extractListItems(section);
|
|
21
7
|
if (listItems.length > 0) {
|
|
@@ -72,18 +58,14 @@ function buildCriterion(text, source, index) {
|
|
|
72
58
|
if (trimmed.length === 0) {
|
|
73
59
|
return null;
|
|
74
60
|
}
|
|
75
|
-
const
|
|
76
|
-
const withoutId = stripCriterionIdPrefix(trimmed);
|
|
77
|
-
const aliases = new Set([
|
|
78
|
-
normalizeCriterionValue(trimmed),
|
|
79
|
-
normalizeCriterionValue(withoutId),
|
|
80
|
-
normalizeCriterionValue(id),
|
|
81
|
-
]);
|
|
61
|
+
const identity = createCriterionIdentity(trimmed, { index });
|
|
82
62
|
return {
|
|
83
|
-
id,
|
|
84
|
-
text:
|
|
63
|
+
id: identity.id,
|
|
64
|
+
text: identity.text,
|
|
65
|
+
normalizedText: identity.normalizedText,
|
|
66
|
+
stableKey: identity.stableKey,
|
|
85
67
|
source,
|
|
86
|
-
aliases:
|
|
68
|
+
aliases: identity.aliases,
|
|
87
69
|
};
|
|
88
70
|
}
|
|
89
71
|
export function extractNormalizedAcceptanceCriteria(raw) {
|
|
@@ -101,6 +83,6 @@ export function extractAcceptanceCriteriaTexts(raw) {
|
|
|
101
83
|
return extractNormalizedAcceptanceCriteria(raw).map((criterion) => criterion.text);
|
|
102
84
|
}
|
|
103
85
|
export function normalizeAcceptanceCriterionToken(value) {
|
|
104
|
-
return
|
|
86
|
+
return normalizeCriterionText(value);
|
|
105
87
|
}
|
|
106
88
|
//# sourceMappingURL=acceptance-criteria.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Spec } from '../../types/index.js';
|
|
2
2
|
import type { CriterionGroundingRecord, GroundingGateResult, TechnicalReferenceGroundingRecord } from '../../types/spec-grounding.js';
|
|
3
|
-
export
|
|
3
|
+
export { normalizeCriterionText } from '../criterion-identity.js';
|
|
4
4
|
export declare function isGenericCriterion(text: string): boolean;
|
|
5
5
|
export declare function filterGroundedCriteria(criteria: string[]): string[];
|
|
6
6
|
export declare function buildCriterionGroundingRecords(args: {
|
|
@@ -21,4 +21,6 @@ export declare function renderTechnicalReferenceGroundingFrontmatter(records: Te
|
|
|
21
21
|
export declare function checkGroundedSpecContract(spec: Spec, content: string): GroundingGateResult;
|
|
22
22
|
export declare function parseCriterionGroundingRecords(frontmatter: string): CriterionGroundingRecord[];
|
|
23
23
|
export declare function parseTechnicalReferenceGroundingRecords(frontmatter: string): TechnicalReferenceGroundingRecord[];
|
|
24
|
+
export declare function parseApprovedFileLevelWorkPlanPaths(content: string): string[];
|
|
25
|
+
export declare function parseGroundedTechnicalPaths(content: string): string[];
|
|
24
26
|
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createCriterionIdentity, normalizeCriterionText } from '../criterion-identity.js';
|
|
2
|
+
export { normalizeCriterionText } from '../criterion-identity.js';
|
|
1
3
|
const GENERIC_CRITERION_PATTERNS = [
|
|
2
4
|
/\bimplementation complete\b/i,
|
|
3
5
|
/\bcomplete and tested\b/i,
|
|
@@ -8,13 +10,6 @@ const GENERIC_CRITERION_PATTERNS = [
|
|
|
8
10
|
/\bdefine acceptance criteria\b/i,
|
|
9
11
|
];
|
|
10
12
|
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/;
|
|
11
|
-
export function normalizeCriterionText(text) {
|
|
12
|
-
return text
|
|
13
|
-
.replace(/^[-*]\s*\[[ xX]\]\s*/, '')
|
|
14
|
-
.replace(/\s+/g, ' ')
|
|
15
|
-
.trim()
|
|
16
|
-
.toLowerCase();
|
|
17
|
-
}
|
|
18
13
|
export function isGenericCriterion(text) {
|
|
19
14
|
return GENERIC_CRITERION_PATTERNS.some((pattern) => pattern.test(text));
|
|
20
15
|
}
|
|
@@ -24,12 +19,16 @@ export function filterGroundedCriteria(criteria) {
|
|
|
24
19
|
export function buildCriterionGroundingRecords(args) {
|
|
25
20
|
const userInput = normalizeCriterionText(args.userInput);
|
|
26
21
|
const generatedEvidence = args.generatedEvidence ?? [];
|
|
27
|
-
const criterionEvidence = new Map(
|
|
28
|
-
|
|
29
|
-
record.evidence.filter((item) => item.trim().length > 0)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
const criterionEvidence = new Map();
|
|
23
|
+
for (const [index, record] of (args.criterionEvidence ?? []).entries()) {
|
|
24
|
+
const evidence = record.evidence.filter((item) => item.trim().length > 0);
|
|
25
|
+
for (const alias of createCriterionIdentity(record.criterion, { index }).aliases) {
|
|
26
|
+
criterionEvidence.set(alias, evidence);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return args.criteria.map((criterion, index) => {
|
|
30
|
+
const identity = createCriterionIdentity(criterion, { index });
|
|
31
|
+
const normalized = identity.normalizedText;
|
|
33
32
|
if (isGenericCriterion(criterion)) {
|
|
34
33
|
return {
|
|
35
34
|
text: criterion,
|
|
@@ -46,7 +45,7 @@ export function buildCriterionGroundingRecords(args) {
|
|
|
46
45
|
confidence: 'high',
|
|
47
46
|
};
|
|
48
47
|
}
|
|
49
|
-
const directProjectEvidence = criterionEvidence.get(
|
|
48
|
+
const directProjectEvidence = identity.aliases.map((alias) => criterionEvidence.get(alias)).find(Boolean) ?? [];
|
|
50
49
|
if (directProjectEvidence.length > 0) {
|
|
51
50
|
return {
|
|
52
51
|
text: criterion,
|
|
@@ -177,6 +176,32 @@ export function parseTechnicalReferenceGroundingRecords(frontmatter) {
|
|
|
177
176
|
}
|
|
178
177
|
return records;
|
|
179
178
|
}
|
|
179
|
+
export function parseApprovedFileLevelWorkPlanPaths(content) {
|
|
180
|
+
const section = /(?:^|\n)### File-Level Work Plan\s*\n([\s\S]*?)(?=\n###\s|$)/i.exec(content)?.[1];
|
|
181
|
+
if (!section) {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
const paths = new Set();
|
|
185
|
+
for (const match of section.matchAll(/^-\s*(?:Create|Modify|Test):\s*`([^`]+)`/gim)) {
|
|
186
|
+
const path = match[1]?.trim() ?? '';
|
|
187
|
+
if (path.length > 0 &&
|
|
188
|
+
!path.startsWith('/') &&
|
|
189
|
+
!path.split('/').includes('..') &&
|
|
190
|
+
!/[*?[\]{}]/.test(path)) {
|
|
191
|
+
paths.add(path.replace(/^\.\//, ''));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return [...paths];
|
|
195
|
+
}
|
|
196
|
+
export function parseGroundedTechnicalPaths(content) {
|
|
197
|
+
const frontmatter = FRONTMATTER_RE.exec(content)?.[1] ?? '';
|
|
198
|
+
return [
|
|
199
|
+
...new Set([
|
|
200
|
+
...parseTechnicalReferenceGroundingRecords(frontmatter).map((record) => record.path),
|
|
201
|
+
...parseApprovedFileLevelWorkPlanPaths(content),
|
|
202
|
+
]),
|
|
203
|
+
];
|
|
204
|
+
}
|
|
180
205
|
function parseYamlInlineStringArray(raw) {
|
|
181
206
|
if (raw.trim().length === 0) {
|
|
182
207
|
return [];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// engine/universal-rules/installer.ts — SPEC-779
|
|
2
2
|
// Install all applicable universal rules for a given host.
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { join, relative } from 'node:path';
|
|
4
4
|
import { atomicWriteFile } from '../safety/atomic-write-file.js';
|
|
5
5
|
import { UNIVERSAL_RULES } from './catalog.js';
|
|
6
6
|
import { writeRuleForHost } from './host-writer.js';
|
|
@@ -38,7 +38,7 @@ export async function installUniversalRules(projectPath, host) {
|
|
|
38
38
|
const hash = hashContent(content);
|
|
39
39
|
const entry = {
|
|
40
40
|
id: rule.id,
|
|
41
|
-
path: writtenPath,
|
|
41
|
+
path: relative(projectPath, writtenPath).split('\\').join('/'),
|
|
42
42
|
hostId: host,
|
|
43
43
|
hashAtInstall: hash,
|
|
44
44
|
installedAt: new Date().toISOString(),
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ValidationEvidenceKeyInput, ValidationToolchain } from '../types/validation-evidence.js';
|
|
2
|
+
export declare function resolveValidationToolchain(input: ValidationEvidenceKeyInput): ValidationToolchain;
|
|
3
|
+
export declare function digestValidationPaths(projectPath: string, selectedPaths: readonly string[]): string;
|
|
4
|
+
//# sourceMappingURL=validation-evidence-digest.d.ts.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
export function resolveValidationToolchain(input) {
|
|
5
|
+
const packageManager = readPackageManager(input.projectPath);
|
|
6
|
+
return {
|
|
7
|
+
node: input.toolchain?.node ?? process.version,
|
|
8
|
+
packageManager: input.toolchain?.packageManager ?? packageManager,
|
|
9
|
+
platform: input.platformDependent
|
|
10
|
+
? (input.toolchain?.platform ?? process.platform)
|
|
11
|
+
: 'portable',
|
|
12
|
+
architecture: input.platformDependent
|
|
13
|
+
? (input.toolchain?.architecture ?? process.arch)
|
|
14
|
+
: 'portable',
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function digestValidationPaths(projectPath, selectedPaths) {
|
|
18
|
+
const root = resolve(projectPath);
|
|
19
|
+
const files = new Set();
|
|
20
|
+
for (const selected of selectedPaths) {
|
|
21
|
+
collectFiles(root, selected, files);
|
|
22
|
+
}
|
|
23
|
+
const hash = createHash('sha256');
|
|
24
|
+
for (const relativePath of [...files].sort()) {
|
|
25
|
+
hash.update(relativePath);
|
|
26
|
+
hash.update('\0');
|
|
27
|
+
try {
|
|
28
|
+
hash.update(readFileSync(join(root, relativePath)));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
hash.update('unreadable');
|
|
32
|
+
}
|
|
33
|
+
hash.update('\0');
|
|
34
|
+
}
|
|
35
|
+
return hash.digest('hex');
|
|
36
|
+
}
|
|
37
|
+
function readPackageManager(projectPath) {
|
|
38
|
+
try {
|
|
39
|
+
const manifest = JSON.parse(readFileSync(join(projectPath, 'package.json'), 'utf-8'));
|
|
40
|
+
return typeof manifest.packageManager === 'string' ? manifest.packageManager : '';
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function collectFiles(root, selected, files) {
|
|
47
|
+
const normalized = normalizeRelativePath(selected);
|
|
48
|
+
if (!normalized || normalized.startsWith('../')) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const absolute = resolve(root, normalized);
|
|
52
|
+
if (absolute !== root && !absolute.startsWith(`${root}${sep}`)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
let stat;
|
|
56
|
+
try {
|
|
57
|
+
stat = lstatSync(absolute);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
files.add(`${normalized}:missing`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (stat.isSymbolicLink()) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (stat.isFile()) {
|
|
67
|
+
files.add(normalized);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!stat.isDirectory()) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
for (const entry of readdirSync(absolute)) {
|
|
74
|
+
if (entry === '.git' || entry === 'node_modules' || entry === 'dist') {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
collectFiles(root, relative(root, join(absolute, entry)), files);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function normalizeRelativePath(path) {
|
|
81
|
+
return path.trim().replaceAll('\\', '/').replace(/^\.\//, '');
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=validation-evidence-digest.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ValidationEvidenceAudit, type ValidationEvidenceIdentity, type ValidationEvidenceKeyInput, type ValidationEvidenceReceipt } from '../types/validation-evidence.js';
|
|
2
|
+
export declare function normalizeValidationCommand(command: string): string;
|
|
3
|
+
export declare function computeValidationEvidenceIdentity(input: ValidationEvidenceKeyInput): ValidationEvidenceIdentity;
|
|
4
|
+
export declare function computeValidationEvidenceKey(identity: ValidationEvidenceIdentity): string;
|
|
5
|
+
export declare function validationReceiptPath(projectId: string, key: string): string;
|
|
6
|
+
export declare function readMatchingValidationEvidence(projectId: string, input: ValidationEvidenceKeyInput): ValidationEvidenceReceipt | null;
|
|
7
|
+
export declare function recordSuccessfulValidationEvidence(args: {
|
|
8
|
+
projectId: string;
|
|
9
|
+
input: ValidationEvidenceKeyInput;
|
|
10
|
+
expectedKey?: string;
|
|
11
|
+
producer: string;
|
|
12
|
+
durationMs: number;
|
|
13
|
+
output: string;
|
|
14
|
+
reason: string;
|
|
15
|
+
source?: string;
|
|
16
|
+
}): ValidationEvidenceReceipt | null;
|
|
17
|
+
export declare function auditValidationEvidence(receipt: ValidationEvidenceReceipt, action: ValidationEvidenceAudit['action']): ValidationEvidenceAudit;
|
|
18
|
+
export declare function recordValidationEvidenceAudit(projectId: string, audit: ValidationEvidenceAudit): void;
|
|
19
|
+
export declare function recordValidationOutcome(args: {
|
|
20
|
+
projectId: string;
|
|
21
|
+
input: ValidationEvidenceKeyInput;
|
|
22
|
+
action: ValidationEvidenceAudit['action'];
|
|
23
|
+
producer: string;
|
|
24
|
+
status: ValidationEvidenceReceipt['status'];
|
|
25
|
+
durationMs: number;
|
|
26
|
+
output: string;
|
|
27
|
+
reason: string;
|
|
28
|
+
source?: string;
|
|
29
|
+
}): void;
|
|
30
|
+
//# sourceMappingURL=validation-evidence-ledger.d.ts.map
|