@planu/cli 5.5.3 → 5.7.0
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 +58 -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/autopilot/bootstrap.js +1 -1
- package/dist/engine/cascade-hooks/core/append-releases.js +22 -12
- package/dist/engine/contradiction-detector.d.ts +2 -1
- package/dist/engine/contradiction-detector.js +215 -0
- package/dist/engine/detection-utils.d.ts +1 -0
- package/dist/engine/detection-utils.js +33 -0
- package/dist/engine/doc-generator/portal/index.d.ts +1 -1
- package/dist/engine/doc-generator/portal/index.js +1 -1
- package/dist/engine/doc-generator/portal/portal-regenerator.d.ts +8 -3
- package/dist/engine/doc-generator/portal/portal-regenerator.js +16 -7
- package/dist/engine/evidence-gates/evidence-autofill.d.ts +1 -1
- package/dist/engine/evidence-gates/evidence-autofill.js +1 -1
- package/dist/engine/framework-detector.js +8 -6
- package/dist/engine/handoff-artifacts/schemas.js +4 -0
- package/dist/engine/housekeeping/history-log.d.ts +1 -0
- package/dist/engine/housekeeping/history-log.js +58 -3
- package/dist/engine/housekeeping/index.d.ts +2 -1
- package/dist/engine/housekeeping/index.js +2 -1
- package/dist/engine/housekeeping/legacy-planu-demolisher.d.ts +3 -0
- package/dist/engine/housekeeping/legacy-planu-demolisher.js +164 -0
- package/dist/engine/housekeeping/runtime-residue-sweep.d.ts +9 -0
- package/dist/engine/housekeeping/runtime-residue-sweep.js +57 -0
- package/dist/engine/lifecycle-reconciliation.js +87 -40
- package/dist/engine/next-spec-resolver/orchestration-planner.js +1 -1
- package/dist/engine/next-spec-resolver/session-writer.js +1 -1
- package/dist/engine/project-graph/cache.js +23 -3
- package/dist/engine/readiness-checker.js +14 -2
- package/dist/engine/sdd-flow/checkpoints.js +29 -2
- package/dist/engine/session/checkpoint-writer.d.ts +1 -1
- package/dist/engine/session/checkpoint-writer.js +6 -5
- package/dist/engine/session-state/writer.js +4 -3
- package/dist/engine/spec-format/lean-spec-generator.js +1 -1
- package/dist/engine/spec-migrator/planu-canonical-policy.d.ts +8 -1
- package/dist/engine/spec-migrator/planu-canonical-policy.js +14 -13
- package/dist/engine/spec-migrator/strict-planu-cleanup.js +28 -3
- 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/universal-rules/rules/planu-release-policy.js +1 -1
- package/dist/engine/validator/spec-compliance-runner.d.ts +2 -1
- package/dist/engine/validator/spec-compliance-runner.js +123 -1
- package/dist/hosts/claude-code/ux/mcp-resources.js +7 -23
- package/dist/index.js +26 -0
- package/dist/resources/specs.js +12 -33
- package/dist/storage/current-project.d.ts +3 -0
- package/dist/storage/current-project.js +21 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/migrations/canonical-storage.js +5 -0
- package/dist/storage/retention.d.ts +14 -0
- package/dist/storage/retention.js +279 -0
- package/dist/storage/spec-index.d.ts +23 -0
- package/dist/storage/spec-index.js +123 -0
- package/dist/storage/spec-store.d.ts +8 -3
- package/dist/storage/spec-store.js +76 -6
- package/dist/storage/storage-catalog.js +3 -3
- package/dist/storage/storage-layout.d.ts +8 -0
- package/dist/storage/storage-layout.js +9 -0
- package/dist/storage/transition-log.js +2 -0
- package/dist/tools/challenge-spec.js +35 -19
- package/dist/tools/create-spec.js +10 -0
- package/dist/tools/execute-sdd-flow.js +11 -2
- package/dist/tools/export-spec.js +2 -1
- package/dist/tools/force-status-analytics.js +2 -1
- package/dist/tools/generate-docs-site.js +2 -1
- package/dist/tools/generate-proposal.js +6 -2
- package/dist/tools/init-project/claude-md-generator.js +19 -2
- package/dist/tools/init-project/conventions-writer.d.ts +5 -2
- package/dist/tools/init-project/conventions-writer.js +18 -13
- package/dist/tools/init-project/git-setup.js +9 -0
- package/dist/tools/init-project/handler.js +9 -1
- 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/legacy-root-migration.d.ts +15 -0
- package/dist/tools/init-project/legacy-root-migration.js +213 -0
- package/dist/tools/init-project/runtime-residue.d.ts +2 -0
- package/dist/tools/init-project/runtime-residue.js +11 -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/session-checkpoint.js +1 -1
- package/dist/tools/update-status/index.js +7 -1
- package/dist/tools/update-status-actions.d.ts +7 -1
- package/dist/tools/update-status-actions.js +10 -2
- package/dist/types/handoff-artifacts.d.ts +1 -0
- package/dist/types/housekeeping.d.ts +38 -0
- package/dist/types/housekeeping.js +0 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/retention.d.ts +12 -0
- package/dist/types/retention.js +3 -0
- package/dist/types/scope.d.ts +23 -0
- package/dist/types/spec/core.d.ts +7 -0
- package/dist/types/spec/index-cache.d.ts +25 -0
- package/dist/types/spec/index-cache.js +3 -0
- package/dist/types/spec/index.d.ts +1 -0
- package/dist/types/spec/index.js +1 -0
- package/dist/types/spec/inputs.d.ts +9 -0
- package/dist/types/spec-format.d.ts +1 -0
- package/dist/types/telemetry.d.ts +39 -1
- package/dist/types/validation-evidence.d.ts +6 -0
- package/package.json +1 -1
- package/planu-plugin.json +1 -1
- package/scripts/lib/pending-release-file.mjs +20 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,61 @@
|
|
|
1
|
+
## [5.7.0] - 2026-08-31
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
- feat: registry-mapped ~/.planu demolisher and storage isolation guard (SPEC-1709)
|
|
5
|
+
- feat(lifecycle): SPEC-1703 declared architectural-premise drift routes reconcile_spec demotion
|
|
6
|
+
- feat: add done-drift missing-files compliance check (SPEC-1701)
|
|
7
|
+
- feat(telemetry): SPEC-1704 anonymous event envelope v1 and consent CLI
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
- fix: serialize prepublish-guard suite to avoid full-load spawn flake
|
|
11
|
+
- fix: declare telemetry and test-home env vars in schema and serialize lock-lease suite
|
|
12
|
+
- fix: reuse shared pathExists helper in legacy planu demolisher (SPEC-1709)
|
|
13
|
+
- fix: harden legacy planu demolisher (symlinks, canonical root) SPEC-1709
|
|
14
|
+
- fix: enforce mcp trust boundary and bind drift source into receipt digest (SPEC-1703)
|
|
15
|
+
- fix: exclude globs and directory refs from done-drift check, wire early return (SPEC-1701)
|
|
16
|
+
- fix(telemetry): close legacy properties, validate ids, truncate show output (SPEC-1704)
|
|
17
|
+
- fix(gates): destination-aware premise matching and tool-level AC1 coverage (SPEC-1702 review fixes)
|
|
18
|
+
- fix(gates): SPEC-1702 cross-spec premise contradiction detection
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## [5.6.0] - 2026-08-31
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
- feat(init-project): regenerate conventions.md from folded conventions.json (SPEC-1699)
|
|
25
|
+
- feat(init-project): fold legacy planu/ root artifacts on update (SPEC-1699)
|
|
26
|
+
- feat(init-project): gitignore legacy planu/ root files (SPEC-1699)
|
|
27
|
+
- feat(create-spec): refresh the regenerable spec index after writing spec.md (SPEC-1699)
|
|
28
|
+
- feat(storage): add regenerable spec.md index (SPEC-1699)
|
|
29
|
+
|
|
30
|
+
### Bug Fixes
|
|
31
|
+
- fix(test): reconcile suites with SPEC-1699 runtime relocation and serialize frozen-lockfile suite
|
|
32
|
+
- fix(storage): classify retention and current-project catch degradations
|
|
33
|
+
- fix(release): require the exact tsgo package dir, not any node_modules content
|
|
34
|
+
- fix(release): drop CI=true from frozen-lockfile install, probe tsgo native package
|
|
35
|
+
- fix(SPEC-1696): accept inline YAML arrays for scenario tests in frontmatter
|
|
36
|
+
- fix(storage): close retention review gaps from Codex CHANGES_REQUIRED
|
|
37
|
+
- fix(storage): enforce retention budgets on runtime storage writers
|
|
38
|
+
- fix(SPEC-1694): report the actual test runner instead of Unknown
|
|
39
|
+
- fix(SPEC-1694): promote framework/database claims only from runtime dependencies
|
|
40
|
+
- fix: parse ordered-list markers in BDD criteria extraction (SPEC-1688)
|
|
41
|
+
- fix: repair release-gate collateral fallout from SPEC-1699 fold (blocker 6 sweep)
|
|
42
|
+
- fix: release.sh repoints version marker to planu/project.json (SPEC-1699 blocker 6)
|
|
43
|
+
- fix: reconcile-release-pending falls back to legacy pending.json (SPEC-1699 blocker 4)
|
|
44
|
+
- fix: legacy-root-migration idempotency and fail-safe malformed handling (SPEC-1699 blocker 3)
|
|
45
|
+
- fix: append-releases fails closed on ledger errors (SPEC-1699 blocker 5)
|
|
46
|
+
- fix: verify spec.md digest at the spec-store read choke point (SPEC-1699 blocker 2)
|
|
47
|
+
- fix(housekeeping): protect planu/.runtime as the canonical runtime home (SPEC-1699)
|
|
48
|
+
- fix: address implementation-review blockers for SPEC-1695 runtime relocation
|
|
49
|
+
- fix: reroute default proposal/export/docs-site outputs to planu/.runtime (SPEC-1695)
|
|
50
|
+
- fix: relocate runtime artifacts to planu/.runtime (SPEC-1695)
|
|
51
|
+
|
|
52
|
+
### Refactoring
|
|
53
|
+
- refactor(release): fold pending release ledger into planu/project.json (SPEC-1699)
|
|
54
|
+
- refactor: relocate session.json and session-context.md under planu/.runtime (SPEC-1699)
|
|
55
|
+
- refactor: route spec resources through the current-project spec index (SPEC-1699)
|
|
56
|
+
- refactor: canonical planu/ set shrinks to project.json + specs/ (SPEC-1699)
|
|
57
|
+
|
|
58
|
+
|
|
1
59
|
## [5.5.3] - 2026-08-30
|
|
2
60
|
|
|
3
61
|
### Bug Fixes
|
package/dist/.planu-build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"schemaVersion":1,"commit":"
|
|
1
|
+
{"schemaVersion":1,"commit":"e145c9f04adb18813ddff699db636c390b20a776"}
|
|
@@ -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",
|
|
@@ -52,7 +52,7 @@ function registerImplementingCascadeListener() {
|
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
// SPEC-649: registerDoneCascadeListener (optimize_context on done) removed — cascade reduced to
|
|
55
|
-
// exactly 2 actions: session.json update +
|
|
55
|
+
// exactly 2 actions: session.json update + project.json releases append.
|
|
56
56
|
/**
|
|
57
57
|
* SPEC-628: Fire-and-forget auto_fix_health on cron:daily.
|
|
58
58
|
* Moved from update_status(done) cascade to reduce latency on done transitions.
|
|
@@ -35,13 +35,17 @@ export function normalizePendingReleaseEntries(value) {
|
|
|
35
35
|
throw new Error('Pending release ledger is not an array');
|
|
36
36
|
}
|
|
37
37
|
const deduped = new Map();
|
|
38
|
+
const preserved = [];
|
|
38
39
|
for (const entry of value) {
|
|
39
40
|
if (!isValidPendingReleaseEntry(entry) || !hasParsableCompletionDate(entry)) {
|
|
41
|
+
// Unknown/invalid shape: never silently drop ledger data — pass it
|
|
42
|
+
// through verbatim so a human can inspect and fix it later.
|
|
43
|
+
preserved.push(entry);
|
|
40
44
|
continue;
|
|
41
45
|
}
|
|
42
46
|
deduped.set(entry.specId, entry);
|
|
43
47
|
}
|
|
44
|
-
return [...deduped.values()];
|
|
48
|
+
return [...preserved, ...deduped.values()];
|
|
45
49
|
}
|
|
46
50
|
export function reconcilePublishedPendingEntries(entries, release, isCommitReachable = requireIsCommitReachable) {
|
|
47
51
|
if (!release) {
|
|
@@ -78,16 +82,23 @@ async function captureHeadCommit(projectPath) {
|
|
|
78
82
|
return undefined;
|
|
79
83
|
}
|
|
80
84
|
}
|
|
85
|
+
function extractReleasesField(parsedProject) {
|
|
86
|
+
return parsedProject !== null &&
|
|
87
|
+
typeof parsedProject === 'object' &&
|
|
88
|
+
!Array.isArray(parsedProject)
|
|
89
|
+
? parsedProject.releases
|
|
90
|
+
: undefined;
|
|
91
|
+
}
|
|
81
92
|
async function rewritePendingLedger(input) {
|
|
82
93
|
let pendingList = [];
|
|
83
94
|
try {
|
|
84
|
-
const raw = await readFile(input.
|
|
85
|
-
pendingList = normalizePendingReleaseEntries(JSON.parse(raw));
|
|
95
|
+
const raw = await readFile(input.projectJsonPath, 'utf-8');
|
|
96
|
+
pendingList = normalizePendingReleaseEntries(extractReleasesField(JSON.parse(raw)) ?? []);
|
|
86
97
|
}
|
|
87
98
|
catch (error) {
|
|
88
99
|
if (error.code !== 'ENOENT') {
|
|
89
100
|
reportClassifiedDegradation('PENDING_RELEASE_LEDGER_UNREADABLE', error);
|
|
90
|
-
|
|
101
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
91
102
|
}
|
|
92
103
|
}
|
|
93
104
|
pendingList = reconcilePublishedPendingEntries(pendingList, input.releaseInfo, input.isCommitReachable).filter((entry) => entry.specId !== input.specId);
|
|
@@ -97,7 +108,7 @@ async function rewritePendingLedger(input) {
|
|
|
97
108
|
completedAt: new Date().toISOString().substring(0, 10),
|
|
98
109
|
...(input.implementationCommit ? { implementationCommit: input.implementationCommit } : {}),
|
|
99
110
|
});
|
|
100
|
-
await input.writePendingReleaseLedger(input.
|
|
111
|
+
await input.writePendingReleaseLedger(input.projectJsonPath, pendingList);
|
|
101
112
|
}
|
|
102
113
|
async function actuallyAppendReleasesPending(ctx, opts) {
|
|
103
114
|
const { projectPath, projectId, specId } = ctx;
|
|
@@ -116,19 +127,19 @@ async function actuallyAppendReleasesPending(ctx, opts) {
|
|
|
116
127
|
return;
|
|
117
128
|
}
|
|
118
129
|
const { isCommitReachable, resolveLatestReleaseTag, withPendingReleaseLock, writePendingReleaseLedger, } = helpers;
|
|
119
|
-
const
|
|
120
|
-
const
|
|
130
|
+
const planuDir = join(projectPath, 'planu');
|
|
131
|
+
const projectJsonPath = join(planuDir, 'project.json');
|
|
121
132
|
await new Promise((resolve, reject) => {
|
|
122
133
|
opts.signal.addEventListener('abort', () => {
|
|
123
134
|
reject(new Error('CoreActionTimeoutError: append-releases-pending timed out'));
|
|
124
135
|
});
|
|
125
136
|
(async () => {
|
|
126
|
-
await mkdir(
|
|
137
|
+
await mkdir(planuDir, { recursive: true });
|
|
127
138
|
const spec = await specStore.getSpec(projectId, specId);
|
|
128
139
|
const releaseInfo = await resolveLatestReleaseTag(projectPath);
|
|
129
140
|
const implementationCommit = await captureHeadCommit(projectPath);
|
|
130
|
-
await withPendingReleaseLock(
|
|
131
|
-
|
|
141
|
+
await withPendingReleaseLock(projectJsonPath, { timeoutMs: 1_500, signal: opts.signal }, () => rewritePendingLedger({
|
|
142
|
+
projectJsonPath,
|
|
132
143
|
specId,
|
|
133
144
|
title: spec?.title ?? specId,
|
|
134
145
|
implementationCommit,
|
|
@@ -150,8 +161,7 @@ async function actuallyAppendReleasesPending(ctx, opts) {
|
|
|
150
161
|
resolve();
|
|
151
162
|
})
|
|
152
163
|
.catch((err) => {
|
|
153
|
-
|
|
154
|
-
resolve();
|
|
164
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
155
165
|
});
|
|
156
166
|
});
|
|
157
167
|
}
|
|
@@ -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
|
|
@@ -4,6 +4,7 @@ export { fileExists };
|
|
|
4
4
|
declare const IGNORE_DIRS: string[];
|
|
5
5
|
export declare function hasAnyFile(basePath: string, patterns: string[]): Promise<boolean>;
|
|
6
6
|
export declare function collectDependencies(projectPath: string): Promise<Set<string>>;
|
|
7
|
+
export declare function collectRuntimeDependencies(projectPath: string): Promise<Set<string>>;
|
|
7
8
|
export declare function loadStackSignatures(): Promise<StackSignatures>;
|
|
8
9
|
export declare function loadArchPatterns(): Promise<ArchPatternsFile>;
|
|
9
10
|
export declare function readJsonFile(filePath: string): Promise<Record<string, unknown> | null>;
|
|
@@ -53,6 +53,17 @@ export async function collectDependencies(projectPath) {
|
|
|
53
53
|
await collectPhpDeps(projectPath, deps);
|
|
54
54
|
return deps;
|
|
55
55
|
}
|
|
56
|
+
export async function collectRuntimeDependencies(projectPath) {
|
|
57
|
+
const deps = new Set();
|
|
58
|
+
await collectNodeRuntimeDeps(projectPath, deps);
|
|
59
|
+
await collectPythonRequirements(projectPath, deps);
|
|
60
|
+
await collectPyprojectDeps(projectPath, deps);
|
|
61
|
+
await collectGoDeps(projectPath, deps);
|
|
62
|
+
await collectRubyDeps(projectPath, deps);
|
|
63
|
+
await collectRustDeps(projectPath, deps);
|
|
64
|
+
await collectPhpRuntimeDeps(projectPath, deps);
|
|
65
|
+
return deps;
|
|
66
|
+
}
|
|
56
67
|
async function collectNodeDeps(projectPath, deps) {
|
|
57
68
|
try {
|
|
58
69
|
const pkgJson = JSON.parse(await readFile(join(projectPath, 'package.json'), 'utf-8'));
|
|
@@ -67,6 +78,17 @@ async function collectNodeDeps(projectPath, deps) {
|
|
|
67
78
|
// No package.json
|
|
68
79
|
}
|
|
69
80
|
}
|
|
81
|
+
async function collectNodeRuntimeDeps(projectPath, deps) {
|
|
82
|
+
try {
|
|
83
|
+
const pkgJson = JSON.parse(await readFile(join(projectPath, 'package.json'), 'utf-8'));
|
|
84
|
+
for (const dep of Object.keys(pkgJson.dependencies ?? {})) {
|
|
85
|
+
deps.add(dep);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// No package.json
|
|
90
|
+
}
|
|
91
|
+
}
|
|
70
92
|
async function collectPythonRequirements(projectPath, deps) {
|
|
71
93
|
try {
|
|
72
94
|
const content = await readFile(join(projectPath, 'requirements.txt'), 'utf-8');
|
|
@@ -181,6 +203,17 @@ async function collectPhpDeps(projectPath, deps) {
|
|
|
181
203
|
// No composer.json
|
|
182
204
|
}
|
|
183
205
|
}
|
|
206
|
+
async function collectPhpRuntimeDeps(projectPath, deps) {
|
|
207
|
+
try {
|
|
208
|
+
const composerJson = JSON.parse(await readFile(join(projectPath, 'composer.json'), 'utf-8'));
|
|
209
|
+
for (const dep of Object.keys(composerJson.require ?? {})) {
|
|
210
|
+
deps.add(dep);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// No composer.json
|
|
215
|
+
}
|
|
216
|
+
}
|
|
184
217
|
export async function loadStackSignatures() {
|
|
185
218
|
try {
|
|
186
219
|
const configPath = new URL('../config/stack-signatures.json', import.meta.url);
|
|
@@ -16,7 +16,7 @@ export { computeVelocity } from './data-aggregators/velocity-aggregator.js';
|
|
|
16
16
|
export { computeBurndown } from './data-aggregators/burndown-aggregator.js';
|
|
17
17
|
export { detectBottlenecks } from './data-aggregators/bottleneck-detector.js';
|
|
18
18
|
export { computeAccuracy } from './data-aggregators/accuracy-aggregator.js';
|
|
19
|
-
export { scheduleRegeneration, regeneratePages } from './portal-regenerator.js';
|
|
19
|
+
export { scheduleRegeneration, regeneratePages, portalPageOutputPath, } from './portal-regenerator.js';
|
|
20
20
|
export { notifyStoreChange } from './regen-hook.js';
|
|
21
21
|
export { getAffectedPages } from './dirty-flag-map.js';
|
|
22
22
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -19,7 +19,7 @@ export { computeBurndown } from './data-aggregators/burndown-aggregator.js';
|
|
|
19
19
|
export { detectBottlenecks } from './data-aggregators/bottleneck-detector.js';
|
|
20
20
|
export { computeAccuracy } from './data-aggregators/accuracy-aggregator.js';
|
|
21
21
|
// SPEC-146 — Auto-regeneration on store changes
|
|
22
|
-
export { scheduleRegeneration, regeneratePages } from './portal-regenerator.js';
|
|
22
|
+
export { scheduleRegeneration, regeneratePages, portalPageOutputPath, } from './portal-regenerator.js';
|
|
23
23
|
export { notifyStoreChange } from './regen-hook.js';
|
|
24
24
|
export { getAffectedPages } from './dirty-flag-map.js';
|
|
25
25
|
//# sourceMappingURL=index.js.map
|