@planu/cli 5.6.0 → 5.7.1
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 +41 -0
- package/dist/.planu-build.json +1 -1
- package/dist/cli/commands/telemetry.d.ts +3 -0
- package/dist/cli/commands/telemetry.js +118 -0
- package/dist/cli/router.js +3 -1
- package/dist/config/environment-schema.json +14 -0
- package/dist/engine/contradiction-detector.d.ts +2 -1
- package/dist/engine/contradiction-detector.js +215 -0
- package/dist/engine/detectors/cache-db-detector.js +2 -2
- package/dist/engine/detectors/newsql-db-detector.js +2 -2
- package/dist/engine/detectors/search-engine-detector.js +2 -2
- package/dist/engine/detectors/vector-db-detector.js +2 -2
- package/dist/engine/detectors/widecolumn-db-detector.js +2 -2
- package/dist/engine/handoff-artifacts/schemas.js +4 -0
- package/dist/engine/housekeeping/legacy-planu-demolisher.d.ts +3 -0
- package/dist/engine/housekeeping/legacy-planu-demolisher.js +164 -0
- package/dist/engine/lifecycle-reconciliation.js +87 -40
- package/dist/engine/readiness-checker.js +13 -1
- package/dist/engine/telemetry/error-reporter.d.ts +9 -9
- package/dist/engine/telemetry/error-reporter.js +15 -34
- package/dist/engine/telemetry/event-envelope.d.ts +11 -0
- package/dist/engine/telemetry/event-envelope.js +124 -0
- package/dist/engine/telemetry/telemetry-client.d.ts +8 -1
- package/dist/engine/telemetry/telemetry-client.js +38 -20
- package/dist/engine/telemetry/telemetry-store.d.ts +15 -2
- package/dist/engine/telemetry/telemetry-store.js +73 -2
- package/dist/engine/validator/reliability-gate.d.ts +4 -0
- package/dist/engine/validator/reliability-gate.js +93 -0
- package/dist/engine/validator/spec-compliance-runner.d.ts +2 -1
- package/dist/engine/validator/spec-compliance-runner.js +78 -1
- package/dist/index.js +26 -0
- package/dist/storage/migrations/canonical-storage.js +22 -9
- package/dist/tools/challenge-spec.js +25 -10
- package/dist/tools/init-project/handler.js +2 -2
- package/dist/tools/init-project/legacy-planu.d.ts +2 -0
- package/dist/tools/init-project/legacy-planu.js +18 -0
- package/dist/tools/init-project/schedule-housekeeping.d.ts +2 -0
- package/dist/tools/init-project/schedule-housekeeping.js +8 -0
- package/dist/tools/reconcile-spec.js +29 -2
- package/dist/tools/register-spec-tools/analysis-tools.d.ts +7 -0
- package/dist/tools/register-spec-tools/analysis-tools.js +13 -1
- package/dist/tools/safe-handler.js +6 -12
- package/dist/tools/validate.js +36 -1
- package/dist/types/handoff-artifacts.d.ts +1 -0
- package/dist/types/housekeeping.d.ts +34 -0
- package/dist/types/housekeeping.js +0 -1
- package/dist/types/scope.d.ts +23 -0
- package/dist/types/spec/inputs.d.ts +9 -0
- package/dist/types/telemetry.d.ts +39 -1
- package/dist/types/validation-evidence.d.ts +18 -0
- package/package.json +1 -1
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
+
## [5.7.1] - 2026-08-31
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
- fix(release): classify cherry-pick abort cleanup for reliability gate
|
|
5
|
+
- fix(test): serialize validate-execute.branches and git-setup suites to avoid full-load spawn flakes
|
|
6
|
+
- fix(test): pin reliability-gate suite in serial inventory snapshot
|
|
7
|
+
- fix(test): mock reliability policies output in lifecycle e2e and serialize reliability-gate suite
|
|
8
|
+
- fix: ignore and untrack runtime index backup files
|
|
9
|
+
- fix: untrack runtime housekeeping history leaked by runtime migration
|
|
10
|
+
- fix(test-infra): address SPEC-1707 review findings in spawn retry helper and lint
|
|
11
|
+
- fix(detectors): resolve datastore claims from runtime dependencies only (SPEC-1700)
|
|
12
|
+
- fix(test-infra): allowlist SPEC-1706 release harness spawner in child_process lint
|
|
13
|
+
- fix(test-infra): add transient spawn retry classifier and child_process lint (SPEC-1707)
|
|
14
|
+
- fix: address SPEC-1706 review findings in release.sh --abort repair
|
|
15
|
+
- fix: repair buried phantom bump commits in release.sh --abort (SPEC-1706)
|
|
16
|
+
- fix(validate): audit spec-declared files and dedupe reliability allowlist
|
|
17
|
+
- fix(validate): run reliability gate on spec-listed files before done (SPEC-1705)
|
|
18
|
+
- fix(storage): exclude atomic-write temp files and tolerate vanish races in inventoryStorageRoot
|
|
19
|
+
- fix: retire completed canonical migration journal on unchanged roots (SPEC-1711)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## [5.7.0] - 2026-08-31
|
|
23
|
+
|
|
24
|
+
### Features
|
|
25
|
+
- feat: registry-mapped ~/.planu demolisher and storage isolation guard (SPEC-1709)
|
|
26
|
+
- feat(lifecycle): SPEC-1703 declared architectural-premise drift routes reconcile_spec demotion
|
|
27
|
+
- feat: add done-drift missing-files compliance check (SPEC-1701)
|
|
28
|
+
- feat(telemetry): SPEC-1704 anonymous event envelope v1 and consent CLI
|
|
29
|
+
|
|
30
|
+
### Bug Fixes
|
|
31
|
+
- fix: serialize prepublish-guard suite to avoid full-load spawn flake
|
|
32
|
+
- fix: declare telemetry and test-home env vars in schema and serialize lock-lease suite
|
|
33
|
+
- fix: reuse shared pathExists helper in legacy planu demolisher (SPEC-1709)
|
|
34
|
+
- fix: harden legacy planu demolisher (symlinks, canonical root) SPEC-1709
|
|
35
|
+
- fix: enforce mcp trust boundary and bind drift source into receipt digest (SPEC-1703)
|
|
36
|
+
- fix: exclude globs and directory refs from done-drift check, wire early return (SPEC-1701)
|
|
37
|
+
- fix(telemetry): close legacy properties, validate ids, truncate show output (SPEC-1704)
|
|
38
|
+
- fix(gates): destination-aware premise matching and tool-level AC1 coverage (SPEC-1702 review fixes)
|
|
39
|
+
- fix(gates): SPEC-1702 cross-spec premise contradiction detection
|
|
40
|
+
|
|
41
|
+
|
|
1
42
|
## [5.6.0] - 2026-08-31
|
|
2
43
|
|
|
3
44
|
### Features
|
package/dist/.planu-build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"schemaVersion":1,"commit":"
|
|
1
|
+
{"schemaVersion":1,"commit":"04661e53067d139151a01671e92fec0f4f2ddfc1"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// cli/commands/telemetry.ts — planu telemetry status|enable|disable|show (SPEC-1704)
|
|
2
|
+
import { isTelemetryEnabled, readTelemetryConfig, enableTelemetry, disableTelemetry, } from '../../engine/telemetry/telemetry-store.js';
|
|
3
|
+
import { buildTelemetryEnvelope, TELEMETRY_SESSION_ID, } from '../../engine/telemetry/event-envelope.js';
|
|
4
|
+
import { bold, dim, green, red } from '../colors.js';
|
|
5
|
+
const NOTICE = 'Planu telemetry, when enabled, sends anonymous usage events (tool name, result, duration ' +
|
|
6
|
+
'bucket, operating system, Node major version, Planu version) tagged with a random per-install ' +
|
|
7
|
+
'id. It never sends prompts, tool arguments or results, spec content, source code, file paths, ' +
|
|
8
|
+
'usernames, hostnames, repository names, environment variables, tokens, or raw stack traces. ' +
|
|
9
|
+
'Run `planu telemetry disable` to opt out anytime.';
|
|
10
|
+
const TRUNCATED_ID_LENGTH = 8;
|
|
11
|
+
const SAMPLE_INSTALLATION_ID = '00000000-0000-4000-8000-000000000000';
|
|
12
|
+
const SAMPLE_DURATION_MS = 450;
|
|
13
|
+
function truncateId(id) {
|
|
14
|
+
if (!id) {
|
|
15
|
+
return '(none)';
|
|
16
|
+
}
|
|
17
|
+
return `${id.slice(0, TRUNCATED_ID_LENGTH)}…`;
|
|
18
|
+
}
|
|
19
|
+
function activeSuppressionFlags() {
|
|
20
|
+
const active = [];
|
|
21
|
+
if (process.env.PLANU_TELEMETRY_DISABLED === '1') {
|
|
22
|
+
active.push('PLANU_TELEMETRY_DISABLED=1');
|
|
23
|
+
}
|
|
24
|
+
if (process.env.DO_NOT_TRACK === '1') {
|
|
25
|
+
active.push('DO_NOT_TRACK=1');
|
|
26
|
+
}
|
|
27
|
+
if (process.env.CI === 'true') {
|
|
28
|
+
active.push('CI=true');
|
|
29
|
+
}
|
|
30
|
+
if (process.env.PLANU_TELEMETRY === 'off') {
|
|
31
|
+
active.push('PLANU_TELEMETRY=off');
|
|
32
|
+
}
|
|
33
|
+
return active;
|
|
34
|
+
}
|
|
35
|
+
async function runStatus(flags) {
|
|
36
|
+
const enabled = await isTelemetryEnabled();
|
|
37
|
+
const config = await readTelemetryConfig();
|
|
38
|
+
const suppressionFlagsActive = activeSuppressionFlags();
|
|
39
|
+
if (flags?.json) {
|
|
40
|
+
process.stdout.write(JSON.stringify({
|
|
41
|
+
enabled,
|
|
42
|
+
consentVersion: config?.consentVersion,
|
|
43
|
+
anonymousInstallationId: truncateId(config?.anonymousInstallationId),
|
|
44
|
+
suppressionFlagsActive,
|
|
45
|
+
}) + '\n');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const lines = [
|
|
49
|
+
`${bold('Telemetry:')} ${enabled ? green('enabled') : red('disabled')}`,
|
|
50
|
+
`${dim('Installation id:')} ${truncateId(config?.anonymousInstallationId)}`,
|
|
51
|
+
];
|
|
52
|
+
if (suppressionFlagsActive.length > 0) {
|
|
53
|
+
lines.push(`${dim('Suppressed by:')} ${suppressionFlagsActive.join(', ')}`);
|
|
54
|
+
}
|
|
55
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
56
|
+
}
|
|
57
|
+
async function runEnable(flags) {
|
|
58
|
+
const config = await enableTelemetry();
|
|
59
|
+
if (flags?.json) {
|
|
60
|
+
process.stdout.write(JSON.stringify({
|
|
61
|
+
enabled: true,
|
|
62
|
+
anonymousInstallationId: truncateId(config.anonymousInstallationId),
|
|
63
|
+
}) + '\n');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
process.stdout.write(`${green('Telemetry enabled.')} ${NOTICE}\n`);
|
|
67
|
+
}
|
|
68
|
+
async function runDisable(flags) {
|
|
69
|
+
await disableTelemetry();
|
|
70
|
+
if (flags?.json) {
|
|
71
|
+
process.stdout.write(JSON.stringify({ enabled: false }) + '\n');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
process.stdout.write(`${red('Telemetry disabled.')} No anonymous usage events will be sent.\n`);
|
|
75
|
+
}
|
|
76
|
+
async function runShow() {
|
|
77
|
+
const config = await readTelemetryConfig();
|
|
78
|
+
const envelope = buildTelemetryEnvelope('mcp_tool_completed', {
|
|
79
|
+
anonymousInstallationId: SAMPLE_INSTALLATION_ID,
|
|
80
|
+
sessionId: TELEMETRY_SESSION_ID,
|
|
81
|
+
toolName: 'planu_status',
|
|
82
|
+
result: 'success',
|
|
83
|
+
durationMs: SAMPLE_DURATION_MS,
|
|
84
|
+
});
|
|
85
|
+
process.stdout.write(`${dim('Current installation id:')} ${truncateId(config?.anonymousInstallationId)}\n`);
|
|
86
|
+
const displayEnvelope = {
|
|
87
|
+
...envelope,
|
|
88
|
+
anonymousInstallationId: truncateId(envelope.anonymousInstallationId),
|
|
89
|
+
};
|
|
90
|
+
process.stdout.write(JSON.stringify(displayEnvelope, null, 2) + '\n');
|
|
91
|
+
}
|
|
92
|
+
export const telemetryCommand = {
|
|
93
|
+
name: 'telemetry',
|
|
94
|
+
description: 'Manage local telemetry consent (status | enable | disable | show)',
|
|
95
|
+
usage: 'planu telemetry <status|enable|disable|show> [--json]',
|
|
96
|
+
async run(args, flags) {
|
|
97
|
+
const subcommand = args[0] ?? 'status';
|
|
98
|
+
switch (subcommand) {
|
|
99
|
+
case 'status':
|
|
100
|
+
await runStatus(flags);
|
|
101
|
+
return;
|
|
102
|
+
case 'enable':
|
|
103
|
+
await runEnable(flags);
|
|
104
|
+
return;
|
|
105
|
+
case 'disable':
|
|
106
|
+
await runDisable(flags);
|
|
107
|
+
return;
|
|
108
|
+
case 'show':
|
|
109
|
+
await runShow();
|
|
110
|
+
return;
|
|
111
|
+
default:
|
|
112
|
+
process.stderr.write(`${red('Error:')} Unknown telemetry subcommand "${subcommand}"\n`);
|
|
113
|
+
process.stderr.write(`Usage: ${telemetryCommand.usage}\n`);
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
//# sourceMappingURL=telemetry.js.map
|
package/dist/cli/router.js
CHANGED
|
@@ -4,7 +4,7 @@ import { PLANU_VERSION } from '../config/version.js';
|
|
|
4
4
|
import { red, bold, cyan, dim } from './colors.js';
|
|
5
5
|
// Lazy-load commands to avoid importing tool handlers when not needed
|
|
6
6
|
async function loadCommands() {
|
|
7
|
-
const [{ initCommand }, { createCommand }, { listCommand }, { statusCommand }, { estimateCommand }, { validateCommand }, { dashboardCommand }, { auditCommand }, { serveCommand }, { handoffCommand }, { packageHandoffCommand }, { watchCommand }, { installCommand }, { doctorCommand }, { uninstallCommand }, { specCommand }, { healthCommand }, { releaseCommand }, { storageCommand }, { inspectCommand }, { offlineCommand }, { jobCommand },] = await Promise.all([
|
|
7
|
+
const [{ initCommand }, { createCommand }, { listCommand }, { statusCommand }, { estimateCommand }, { validateCommand }, { dashboardCommand }, { auditCommand }, { serveCommand }, { handoffCommand }, { packageHandoffCommand }, { watchCommand }, { installCommand }, { doctorCommand }, { uninstallCommand }, { specCommand }, { healthCommand }, { releaseCommand }, { storageCommand }, { inspectCommand }, { offlineCommand }, { jobCommand }, { telemetryCommand },] = await Promise.all([
|
|
8
8
|
import('./commands/init.js'),
|
|
9
9
|
import('./commands/create.js'),
|
|
10
10
|
import('./commands/list.js'),
|
|
@@ -27,6 +27,7 @@ async function loadCommands() {
|
|
|
27
27
|
import('./commands/inspect.js'),
|
|
28
28
|
import('./commands/offline.js'),
|
|
29
29
|
import('./commands/job.js'),
|
|
30
|
+
import('./commands/telemetry.js'),
|
|
30
31
|
]);
|
|
31
32
|
const cmds = [
|
|
32
33
|
initCommand,
|
|
@@ -51,6 +52,7 @@ async function loadCommands() {
|
|
|
51
52
|
inspectCommand,
|
|
52
53
|
offlineCommand,
|
|
53
54
|
jobCommand,
|
|
55
|
+
telemetryCommand,
|
|
54
56
|
];
|
|
55
57
|
const map = new Map();
|
|
56
58
|
for (const cmd of cmds) {
|
|
@@ -218,6 +218,13 @@
|
|
|
218
218
|
"sensitivity": "private",
|
|
219
219
|
"default": null
|
|
220
220
|
},
|
|
221
|
+
{
|
|
222
|
+
"name": "PLANU_TELEMETRY_DISABLED",
|
|
223
|
+
"type": "boolean",
|
|
224
|
+
"scope": "runtime",
|
|
225
|
+
"sensitivity": "public",
|
|
226
|
+
"default": null
|
|
227
|
+
},
|
|
221
228
|
{
|
|
222
229
|
"name": "PLANU_TELEMETRY_ENDPOINT",
|
|
223
230
|
"type": "string",
|
|
@@ -232,6 +239,13 @@
|
|
|
232
239
|
"sensitivity": "secret",
|
|
233
240
|
"default": null
|
|
234
241
|
},
|
|
242
|
+
{
|
|
243
|
+
"name": "PLANU_TEST_HOME",
|
|
244
|
+
"type": "string",
|
|
245
|
+
"scope": "test",
|
|
246
|
+
"sensitivity": "public",
|
|
247
|
+
"default": null
|
|
248
|
+
},
|
|
235
249
|
{
|
|
236
250
|
"name": "PLANU_TIMING",
|
|
237
251
|
"type": "boolean",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ConflictReport, Contradiction, SpecAnalysisInput } from '../types/index.js';
|
|
1
|
+
import type { ConflictReport, Contradiction, CrossSpecPremiseFinding, SpecAnalysisInput } from '../types/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Detect semantic contradictions between a list of specs.
|
|
4
4
|
* Each spec is compared against every other spec for all known patterns.
|
|
@@ -12,4 +12,5 @@ export declare function buildConflictReport(specs: SpecAnalysisInput[]): Conflic
|
|
|
12
12
|
* Format a ConflictReport as a markdown string.
|
|
13
13
|
*/
|
|
14
14
|
export declare function formatConflictReport(report: ConflictReport): string;
|
|
15
|
+
export declare function detectCrossSpecPremiseContradictions(targetSpecId: string, targetRawContent: string, projectPath: string | undefined): Promise<CrossSpecPremiseFinding[]>;
|
|
15
16
|
//# sourceMappingURL=contradiction-detector.d.ts.map
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// engine/contradiction-detector.ts — Semantic contradiction detection (SPEC-038)
|
|
2
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { findMarkdownSectionRange } from './spec-format/markdown-sections.js';
|
|
5
|
+
import { getUniqueTopLevelFrontmatterString, stripFrontmatter } from './frontmatter-parser.js';
|
|
1
6
|
// ---------------------------------------------------------------------------
|
|
2
7
|
// Contradiction patterns
|
|
3
8
|
// ---------------------------------------------------------------------------
|
|
@@ -155,4 +160,214 @@ export function formatConflictReport(report) {
|
|
|
155
160
|
}
|
|
156
161
|
return lines.join('\n');
|
|
157
162
|
}
|
|
163
|
+
const DIRECTIONAL_PHRASE_PAIRS = [
|
|
164
|
+
{ phraseA: 'out of', phraseB: 'into' },
|
|
165
|
+
{ phraseA: 're-roots at', phraseB: 'relocates to' },
|
|
166
|
+
{ phraseA: 'removes', phraseB: 'adds' },
|
|
167
|
+
{ phraseA: 'enables', phraseB: 'disables' },
|
|
168
|
+
];
|
|
169
|
+
const SIGNIFICANT_WORD_STOPWORDS = new Set([
|
|
170
|
+
'this',
|
|
171
|
+
'that',
|
|
172
|
+
'with',
|
|
173
|
+
'from',
|
|
174
|
+
'into',
|
|
175
|
+
'spec',
|
|
176
|
+
'specs',
|
|
177
|
+
'after',
|
|
178
|
+
'before',
|
|
179
|
+
'while',
|
|
180
|
+
'their',
|
|
181
|
+
'them',
|
|
182
|
+
'about',
|
|
183
|
+
'have',
|
|
184
|
+
'will',
|
|
185
|
+
'when',
|
|
186
|
+
'then',
|
|
187
|
+
'also',
|
|
188
|
+
'were',
|
|
189
|
+
'been',
|
|
190
|
+
'these',
|
|
191
|
+
'those',
|
|
192
|
+
]);
|
|
193
|
+
function splitBodyIntoSentenceFragments(body) {
|
|
194
|
+
const sentences = [];
|
|
195
|
+
for (const line of body.split('\n')) {
|
|
196
|
+
const trimmedLine = line.trim();
|
|
197
|
+
if (trimmedLine.length === 0) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
for (const clause of trimmedLine.split(/(?<=[.!?])\s+/)) {
|
|
201
|
+
const sentence = clause.trim();
|
|
202
|
+
if (sentence.length > 0) {
|
|
203
|
+
sentences.push(sentence);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return sentences;
|
|
208
|
+
}
|
|
209
|
+
/** Extracts distinct "SPEC-NNNN" references from body text, excluding the spec's own ID. */
|
|
210
|
+
function extractReferencedSpecIds(body, excludeSpecId) {
|
|
211
|
+
const ids = new Set();
|
|
212
|
+
for (const match of body.matchAll(/\bSPEC-(\d+)\b/gi)) {
|
|
213
|
+
const number = match[1];
|
|
214
|
+
if (!number) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const id = `SPEC-${number}`;
|
|
218
|
+
if (id.toUpperCase() !== excludeSpecId.toUpperCase()) {
|
|
219
|
+
ids.add(id);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return [...ids];
|
|
223
|
+
}
|
|
224
|
+
/** Extracts backtick-quoted file paths declared under a spec's "## Files" section. */
|
|
225
|
+
function extractFilesSectionPaths(body) {
|
|
226
|
+
const section = findMarkdownSectionRange(body, 'Files');
|
|
227
|
+
const paths = new Set();
|
|
228
|
+
if (!section) {
|
|
229
|
+
return paths;
|
|
230
|
+
}
|
|
231
|
+
const sectionText = body.slice(section.contentStart, section.end);
|
|
232
|
+
for (const match of sectionText.matchAll(/`([\w./-]+\.[A-Za-z0-9]+)`/g)) {
|
|
233
|
+
const path = match[1];
|
|
234
|
+
if (path) {
|
|
235
|
+
paths.add(path);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return paths;
|
|
239
|
+
}
|
|
240
|
+
function extractSignificantWords(sentence) {
|
|
241
|
+
const words = sentence.toLowerCase().match(/[a-z][a-z0-9._/-]{3,}/g) ?? [];
|
|
242
|
+
return new Set(words.filter((word) => !SIGNIFICANT_WORD_STOPWORDS.has(word)));
|
|
243
|
+
}
|
|
244
|
+
function shareSignificantWord(sentenceA, sentenceB) {
|
|
245
|
+
const wordsA = extractSignificantWords(sentenceA);
|
|
246
|
+
for (const word of extractSignificantWords(sentenceB)) {
|
|
247
|
+
if (wordsA.has(word)) {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
const DESTINATION_TOKEN_PATTERN = /^`([^`]+)`|^([\w][\w./-]*)/;
|
|
254
|
+
function normalizeDestinationToken(token) {
|
|
255
|
+
return token.replace(/`/g, '').replace(/\/+$/, '').toLowerCase();
|
|
256
|
+
}
|
|
257
|
+
function extractDestinationToken(sentence, phrase) {
|
|
258
|
+
const phraseIndex = sentence.toLowerCase().indexOf(phrase);
|
|
259
|
+
if (phraseIndex === -1) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
const after = sentence.slice(phraseIndex + phrase.length).trimStart();
|
|
263
|
+
const match = DESTINATION_TOKEN_PATTERN.exec(after);
|
|
264
|
+
const token = match?.[1] ?? match?.[2];
|
|
265
|
+
if (!token) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
const isBacktickQuoted = match?.[1] !== undefined;
|
|
269
|
+
if (!isBacktickQuoted && !/[./]/.test(token)) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
return normalizeDestinationToken(token);
|
|
273
|
+
}
|
|
274
|
+
function sameDestination(targetSentence, targetPhrase, siblingSentence, siblingPhrase) {
|
|
275
|
+
const targetDestination = extractDestinationToken(targetSentence, targetPhrase);
|
|
276
|
+
const siblingDestination = extractDestinationToken(siblingSentence, siblingPhrase);
|
|
277
|
+
return (targetDestination !== null &&
|
|
278
|
+
siblingDestination !== null &&
|
|
279
|
+
targetDestination === siblingDestination);
|
|
280
|
+
}
|
|
281
|
+
function findOppositeDirectionSentence(targetSentence, siblingSentences) {
|
|
282
|
+
const lowerTarget = targetSentence.toLowerCase();
|
|
283
|
+
for (const pair of DIRECTIONAL_PHRASE_PAIRS) {
|
|
284
|
+
const targetHasA = lowerTarget.includes(pair.phraseA);
|
|
285
|
+
const targetPhrase = targetHasA ? pair.phraseA : pair.phraseB;
|
|
286
|
+
const oppositePhrase = targetHasA ? pair.phraseB : pair.phraseA;
|
|
287
|
+
if (!targetHasA && !lowerTarget.includes(pair.phraseB)) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
for (const siblingSentence of siblingSentences) {
|
|
291
|
+
if (siblingSentence.toLowerCase().includes(oppositePhrase) &&
|
|
292
|
+
shareSignificantWord(targetSentence, siblingSentence) &&
|
|
293
|
+
!sameDestination(targetSentence, targetPhrase, siblingSentence, oppositePhrase)) {
|
|
294
|
+
return siblingSentence;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
async function readSiblingSpecMarkdown(projectPath, siblingSpecId) {
|
|
301
|
+
const specsDir = join(projectPath, 'planu', 'specs');
|
|
302
|
+
let entries;
|
|
303
|
+
try {
|
|
304
|
+
entries = await readdir(specsDir);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
const dirPrefix = `${siblingSpecId}-`.toLowerCase();
|
|
310
|
+
const dirName = entries.find((entry) => entry.toLowerCase() === siblingSpecId.toLowerCase() ||
|
|
311
|
+
entry.toLowerCase().startsWith(dirPrefix));
|
|
312
|
+
if (!dirName) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
return await readFile(join(specsDir, dirName, 'spec.md'), 'utf-8');
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
export async function detectCrossSpecPremiseContradictions(targetSpecId, targetRawContent, projectPath) {
|
|
323
|
+
if (!projectPath) {
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
const targetBody = stripFrontmatter(targetRawContent);
|
|
327
|
+
const referencedSpecIds = extractReferencedSpecIds(targetBody, targetSpecId);
|
|
328
|
+
if (referencedSpecIds.length === 0) {
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
const targetFilePaths = extractFilesSectionPaths(targetBody);
|
|
332
|
+
if (targetFilePaths.size === 0) {
|
|
333
|
+
return [];
|
|
334
|
+
}
|
|
335
|
+
const targetSentences = splitBodyIntoSentenceFragments(targetBody);
|
|
336
|
+
const findings = [];
|
|
337
|
+
for (const siblingSpecId of referencedSpecIds) {
|
|
338
|
+
const siblingRawContent = await readSiblingSpecMarkdown(projectPath, siblingSpecId);
|
|
339
|
+
if (!siblingRawContent) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const siblingStatus = getUniqueTopLevelFrontmatterString(siblingRawContent, 'status');
|
|
343
|
+
if (siblingStatus !== 'done' && siblingStatus !== 'approved') {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const siblingBody = stripFrontmatter(siblingRawContent);
|
|
347
|
+
const siblingFilePaths = extractFilesSectionPaths(siblingBody);
|
|
348
|
+
const sharedFilePath = [...targetFilePaths].find((path) => siblingFilePaths.has(path));
|
|
349
|
+
if (!sharedFilePath) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const claimSentences = targetSentences.filter((sentence) => sentence.toUpperCase().includes(siblingSpecId.toUpperCase()));
|
|
353
|
+
if (claimSentences.length === 0) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const siblingSentences = splitBodyIntoSentenceFragments(siblingBody);
|
|
357
|
+
for (const targetSentence of claimSentences) {
|
|
358
|
+
const siblingSentence = findOppositeDirectionSentence(targetSentence, siblingSentences);
|
|
359
|
+
if (siblingSentence) {
|
|
360
|
+
findings.push({
|
|
361
|
+
targetSpecId,
|
|
362
|
+
siblingSpecId,
|
|
363
|
+
targetSentence,
|
|
364
|
+
siblingSentence,
|
|
365
|
+
sharedFilePath,
|
|
366
|
+
});
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return findings;
|
|
372
|
+
}
|
|
158
373
|
//# sourceMappingURL=contradiction-detector.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { technologyValue } from '../technology-registry.js';
|
|
2
|
-
import {
|
|
2
|
+
import { collectRuntimeDependencies, hasAnyFile } from '../detection-utils.js';
|
|
3
3
|
const VALKEY_DEPS = ['valkey', '@valkey/client', 'iovalkey'];
|
|
4
4
|
const REDIS_DEPS = [
|
|
5
5
|
technologyValue('technology-redis-34fb46'),
|
|
@@ -17,7 +17,7 @@ const REDIS_DEPS = [
|
|
|
17
17
|
];
|
|
18
18
|
const MEMCACHED_DEPS = ['memcached', 'memcachier', 'pymemcache', 'gomemcache', 'memcache', 'dalli'];
|
|
19
19
|
export async function detectCacheEngine(projectPath) {
|
|
20
|
-
const deps = await
|
|
20
|
+
const deps = await collectRuntimeDependencies(projectPath);
|
|
21
21
|
// Valkey first (Redis fork — more specific)
|
|
22
22
|
if (VALKEY_DEPS.some((d) => deps.has(d))) {
|
|
23
23
|
return 'valkey';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { collectRuntimeDependencies } from '../detection-utils.js';
|
|
2
2
|
const PLANETSCALE_DEPS = ['@planetscale/database', 'planetscale-go'];
|
|
3
3
|
const NEON_DEPS = ['@neondatabase/serverless', 'neon-serverless'];
|
|
4
4
|
const SPANNER_DEPS = [
|
|
@@ -10,7 +10,7 @@ const ALLOYDB_DEPS = ['@google-cloud/alloydb', 'google-cloud-alloydb-connector']
|
|
|
10
10
|
const COCKROACH_DEPS = ['cockroachdb', '@cockroachlabs/serverless'];
|
|
11
11
|
const TIDB_DEPS = ['tidb', 'tidb-serverless', '@tidbcloud/serverless'];
|
|
12
12
|
export async function detectNewSqlDb(projectPath) {
|
|
13
|
-
const deps = await
|
|
13
|
+
const deps = await collectRuntimeDependencies(projectPath);
|
|
14
14
|
// PlanetScale (specific SDK)
|
|
15
15
|
if (PLANETSCALE_DEPS.some((d) => deps.has(d))) {
|
|
16
16
|
return 'planetscale';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { collectRuntimeDependencies } from '../detection-utils.js';
|
|
2
2
|
const ALGOLIA_DEPS = [
|
|
3
3
|
'algoliasearch',
|
|
4
4
|
'@algolia/client-search',
|
|
@@ -25,7 +25,7 @@ const ELASTICSEARCH_DEPS = [
|
|
|
25
25
|
'chewy',
|
|
26
26
|
];
|
|
27
27
|
export async function detectSearchEngine(projectPath) {
|
|
28
|
-
const deps = await
|
|
28
|
+
const deps = await collectRuntimeDependencies(projectPath);
|
|
29
29
|
// Algolia (most specific deps)
|
|
30
30
|
if (ALGOLIA_DEPS.some((d) => deps.has(d))) {
|
|
31
31
|
return 'algolia';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { collectRuntimeDependencies } from '../detection-utils.js';
|
|
2
2
|
const PINECONE_DEPS = ['@pinecone-database/pinecone', 'pinecone-client', 'pinecone'];
|
|
3
3
|
const WEAVIATE_DEPS = ['weaviate-client', 'weaviate-ts-client', 'weaviate'];
|
|
4
4
|
const QDRANT_DEPS = ['@qdrant/js-client-rest', 'qdrant-client', 'qdrant_client'];
|
|
@@ -6,7 +6,7 @@ const MILVUS_DEPS = ['@zilliz/milvus2-sdk-node', 'pymilvus', 'milvus'];
|
|
|
6
6
|
const CHROMA_DEPS = ['chromadb', 'chromadb-client'];
|
|
7
7
|
const PGVECTOR_DEPS = ['pgvector', 'pgvector-node', 'pgvector-python'];
|
|
8
8
|
export async function detectVectorDb(projectPath) {
|
|
9
|
-
const deps = await
|
|
9
|
+
const deps = await collectRuntimeDependencies(projectPath);
|
|
10
10
|
if (PINECONE_DEPS.some((d) => deps.has(d))) {
|
|
11
11
|
return 'pinecone';
|
|
12
12
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { collectRuntimeDependencies } from '../detection-utils.js';
|
|
2
2
|
const SCYLLA_DEPS = ['scylladb', '@scylladb/driver', 'scylla-driver'];
|
|
3
3
|
const CASSANDRA_DEPS = [
|
|
4
4
|
'cassandra-driver',
|
|
@@ -16,7 +16,7 @@ const CLICKHOUSE_DEPS = [
|
|
|
16
16
|
];
|
|
17
17
|
const HBASE_DEPS = ['hbase', 'hbase-client', 'happybase', 'hbase-rpc-client'];
|
|
18
18
|
export async function detectWideColumnDb(projectPath) {
|
|
19
|
-
const deps = await
|
|
19
|
+
const deps = await collectRuntimeDependencies(projectPath);
|
|
20
20
|
// ScyllaDB (Cassandra-compatible but more specific)
|
|
21
21
|
if (SCYLLA_DEPS.some((d) => deps.has(d))) {
|
|
22
22
|
return 'scylladb';
|
|
@@ -168,6 +168,10 @@ export const ReconciliationReceiptV1Schema = z.object({
|
|
|
168
168
|
transitionId: z.string().min(1).optional(),
|
|
169
169
|
phase: z.enum(['prepared', 'status-committed', 'spec-synced', 'audit-appended']).optional(),
|
|
170
170
|
auditPending: z.boolean().optional(),
|
|
171
|
+
driftSource: z
|
|
172
|
+
.literal('declared-architectural-premise')
|
|
173
|
+
.optional()
|
|
174
|
+
.describe('Set when the reconciliation demotion evidence is an orchestrator-declared drift rather than an automated validation-report finding.'),
|
|
171
175
|
});
|
|
172
176
|
// ---------------------------------------------------------------------------
|
|
173
177
|
// Schema registry
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { LegacyPlanuDemolisherOptions, LegacyPlanuDemolitionReport } from '../../types/housekeeping.js';
|
|
2
|
+
export declare function demolishLegacyPlanuRoot(options?: LegacyPlanuDemolisherOptions): Promise<LegacyPlanuDemolitionReport>;
|
|
3
|
+
//# sourceMappingURL=legacy-planu-demolisher.d.ts.map
|