@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.
Files changed (91) hide show
  1. package/CHANGELOG.md +129 -22
  2. package/dist/engine/abortable-process-runner.d.ts +8 -0
  3. package/dist/engine/abortable-process-runner.js +107 -0
  4. package/dist/engine/cascade-hooks/core/append-releases.d.ts +1 -0
  5. package/dist/engine/cascade-hooks/core/append-releases.js +51 -14
  6. package/dist/engine/cascade-hooks/hooks/orchestration-plan.hook.js +16 -21
  7. package/dist/engine/ci-generator/yaml-builder.js +5 -1
  8. package/dist/engine/context-md-generator.js +3 -1
  9. package/dist/engine/criterion-identity.d.ts +6 -0
  10. package/dist/engine/criterion-identity.js +63 -0
  11. package/dist/engine/evidence-gates/lifecycle-gate.js +22 -44
  12. package/dist/engine/evidence-index/index-builder.js +7 -19
  13. package/dist/engine/frontmatter-parser.d.ts +5 -0
  14. package/dist/engine/frontmatter-parser.js +24 -0
  15. package/dist/engine/project-command-runtime.d.ts +5 -0
  16. package/dist/{tools/validate-runtime.js → engine/project-command-runtime.js} +8 -21
  17. package/dist/engine/project-graph/builder.js +1 -35
  18. package/dist/engine/project-graph/cache.js +37 -1
  19. package/dist/engine/qa-gate.d.ts +4 -1
  20. package/dist/engine/qa-gate.js +72 -59
  21. package/dist/engine/safety/cross-process-lock.d.ts +1 -6
  22. package/dist/engine/safety/cross-process-lock.js +18 -16
  23. package/dist/engine/safety/lock-scavenger.d.ts +9 -0
  24. package/dist/engine/safety/lock-scavenger.js +114 -0
  25. package/dist/engine/session-context-generator.js +24 -2
  26. package/dist/engine/session-safeguard/learnings-buffer.js +7 -2
  27. package/dist/engine/session-state/writer.js +15 -8
  28. package/dist/engine/skill-registry/installer.js +1 -1
  29. package/dist/engine/spec-format/acceptance-criteria.js +8 -26
  30. package/dist/engine/spec-grounding/contract.d.ts +3 -1
  31. package/dist/engine/spec-grounding/contract.js +39 -14
  32. package/dist/engine/universal-rules/installer.js +2 -2
  33. package/dist/engine/validation-evidence-digest.d.ts +4 -0
  34. package/dist/engine/validation-evidence-digest.js +83 -0
  35. package/dist/engine/validation-evidence-ledger.d.ts +30 -0
  36. package/dist/engine/validation-evidence-ledger.js +274 -0
  37. package/dist/engine/validation-impact-planner.d.ts +4 -0
  38. package/dist/engine/validation-impact-planner.js +71 -0
  39. package/dist/engine/validator/spec-compliance-runner.d.ts +1 -1
  40. package/dist/engine/validator/spec-compliance-runner.js +37 -40
  41. package/dist/engine/validator/validation-report-writer.d.ts +1 -0
  42. package/dist/engine/validator/validation-report-writer.js +1 -1
  43. package/dist/engine/validator.js +7 -11
  44. package/dist/storage/convention-baseline.js +5 -0
  45. package/dist/storage/session-state-store.js +13 -6
  46. package/dist/storage/spec-store.d.ts +5 -0
  47. package/dist/storage/spec-store.js +7 -0
  48. package/dist/tools/git/git-helpers.d.ts +5 -1
  49. package/dist/tools/git/git-helpers.js +3 -2
  50. package/dist/tools/init-project/conventions-writer.js +1 -1
  51. package/dist/tools/init-project/scaffold-writer.js +1 -1
  52. package/dist/tools/safe-handler.d.ts +5 -1
  53. package/dist/tools/safe-handler.js +73 -12
  54. package/dist/tools/status-handler.d.ts +1 -1
  55. package/dist/tools/status-handler.js +27 -66
  56. package/dist/tools/update-status/dod-gates.js +7 -31
  57. package/dist/tools/update-status/evidence-gate.js +2 -4
  58. package/dist/tools/update-status/force-done-audit.d.ts +2 -0
  59. package/dist/tools/update-status/force-done-audit.js +23 -0
  60. package/dist/tools/update-status/qa-gate.d.ts +1 -0
  61. package/dist/tools/update-status/qa-gate.js +7 -51
  62. package/dist/tools/update-status-convention-gate.js +132 -97
  63. package/dist/tools/validate-lint.d.ts +1 -1
  64. package/dist/tools/validate-lint.js +78 -165
  65. package/dist/tools/validate.js +3 -1
  66. package/dist/transports/http-transport.js +22 -8
  67. package/dist/transports/mcp-types.d.ts +2 -2
  68. package/dist/transports/transport-factory.js +110 -12
  69. package/dist/types/abortable-process.d.ts +26 -0
  70. package/dist/types/abortable-process.js +2 -0
  71. package/dist/types/criterion-identity.d.ts +12 -0
  72. package/dist/types/criterion-identity.js +2 -0
  73. package/dist/types/index.d.ts +2 -0
  74. package/dist/types/index.js +2 -0
  75. package/dist/types/qa-gate.d.ts +0 -5
  76. package/dist/types/spec-format.d.ts +2 -0
  77. package/dist/types/spec-lock-v2.d.ts +7 -0
  78. package/dist/types/validation-evidence.d.ts +62 -0
  79. package/dist/types/validation-evidence.js +2 -0
  80. package/package.json +43 -23
  81. package/planu-native.json +1 -1
  82. package/planu-plugin.json +1 -1
  83. package/dist/storage/qa-gate-store.d.ts +0 -4
  84. package/dist/storage/qa-gate-store.js +0 -24
  85. package/dist/tools/validate-runtime.d.ts +0 -14
  86. package/dist/types/data/estimation.d.ts +0 -147
  87. package/dist/types/data/estimation.js +0 -2
  88. package/dist/types/data/index.d.ts +0 -5
  89. package/dist/types/data/index.js +0 -6
  90. package/dist/types/data/velocity.d.ts +0 -168
  91. 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)} specs done / ${String(approvedCount)} approved pending`,
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 = MEMORY_MD_PATH;
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
- const terminalSpecIds = new Set(specs
147
- .filter((spec) => spec.status === 'done' || spec.status === 'discarded')
148
- .map((spec) => spec.id));
149
- index.activeSpecs = index.activeSpecs.filter((specId) => !terminalSpecIds.has(specId));
150
- index.epicProgress = epicProgress;
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
- function normalizeCriterionValue(value) {
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 id = detectCriterionId(trimmed, index);
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: withoutId,
63
+ id: identity.id,
64
+ text: identity.text,
65
+ normalizedText: identity.normalizedText,
66
+ stableKey: identity.stableKey,
85
67
  source,
86
- aliases: [...aliases].filter((alias) => alias.length > 0),
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 normalizeCriterionValue(stripCriterionIdPrefix(value));
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 declare function normalizeCriterionText(text: string): string;
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((args.criterionEvidence ?? []).map((record) => [
28
- normalizeCriterionText(record.criterion),
29
- record.evidence.filter((item) => item.trim().length > 0),
30
- ]));
31
- return args.criteria.map((criterion) => {
32
- const normalized = normalizeCriterionText(criterion);
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(normalized) ?? [];
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