@lsctech/polaris 0.1.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/README.md +87 -0
- package/dist/__mocks__/tool-server-linear.js +34 -0
- package/dist/cli/adopt-approve.js +60 -0
- package/dist/cli/adopt-cognition.js +199 -0
- package/dist/cli/adopt-instructions.js +143 -0
- package/dist/cli/adopt-rules.js +169 -0
- package/dist/cli/adopt-scan.js +25 -0
- package/dist/cli/adopt-smartdocs.js +192 -0
- package/dist/cli/adoption-inventory.js +350 -0
- package/dist/cli/adoption-plan.js +222 -0
- package/dist/cli/args.js +28 -0
- package/dist/cli/args.test.js +30 -0
- package/dist/cli/branding.js +41 -0
- package/dist/cli/graph.js +358 -0
- package/dist/cli/index.js +127 -0
- package/dist/cli/index.test.js +162 -0
- package/dist/cli/init-detect.js +61 -0
- package/dist/cli/init.js +492 -0
- package/dist/cli/init.test.js +259 -0
- package/dist/cli/librarian.js +32 -0
- package/dist/cli/medic.js +96 -0
- package/dist/cli/startup.test.js +23 -0
- package/dist/cli/tracker.js +130 -0
- package/dist/cli/tracker.test.js +126 -0
- package/dist/cli/version.js +10 -0
- package/dist/cli/version.test.js +11 -0
- package/dist/cli/worker.js +287 -0
- package/dist/cluster-state/fixtures/test-clusters.json +19 -0
- package/dist/cluster-state/store.js +302 -0
- package/dist/cluster-state/store.test.js +145 -0
- package/dist/cluster-state/types.js +2 -0
- package/dist/cognition/archive.js +158 -0
- package/dist/cognition/closeout-librarian-types.js +161 -0
- package/dist/cognition/cognition.test.js +623 -0
- package/dist/cognition/index.js +41 -0
- package/dist/cognition/librarian-dispatch.js +391 -0
- package/dist/cognition/librarian-dispatch.test.js +247 -0
- package/dist/cognition/librarian-packet.js +313 -0
- package/dist/cognition/librarian-types.js +57 -0
- package/dist/cognition/route-cognition-delta.js +350 -0
- package/dist/cognition/summary-delta.js +279 -0
- package/dist/cognition/validate.js +162 -0
- package/dist/config/defaults.js +98 -0
- package/dist/config/doctor.js +273 -0
- package/dist/config/index.js +14 -0
- package/dist/config/loader.js +58 -0
- package/dist/config/provider-detect.js +60 -0
- package/dist/config/provider-detect.test.js +119 -0
- package/dist/config/schema.js +2 -0
- package/dist/config/show.js +44 -0
- package/dist/config/show.test.js +42 -0
- package/dist/config/validator.js +675 -0
- package/dist/config/validator.test.js +77 -0
- package/dist/docs/canon-check.js +334 -0
- package/dist/docs/canon-check.test.js +207 -0
- package/dist/docs/doctrine.js +133 -0
- package/dist/docs/doctrine.test.js +139 -0
- package/dist/docs/index.js +168 -0
- package/dist/docs/index.test.js +83 -0
- package/dist/docs/ingest.js +295 -0
- package/dist/docs/ingest.test.js +71 -0
- package/dist/docs/migrate.js +165 -0
- package/dist/docs/migrate.test.js +136 -0
- package/dist/docs/seed-instructions.js +147 -0
- package/dist/docs/seed-instructions.test.js +137 -0
- package/dist/docs/validate-instructions.js +267 -0
- package/dist/docs/validate-instructions.test.js +238 -0
- package/dist/finalize/artifact-policy.js +214 -0
- package/dist/finalize/artifact-policy.test.js +84 -0
- package/dist/finalize/delivery-integrity.js +141 -0
- package/dist/finalize/finalize.test.js +210 -0
- package/dist/finalize/github.js +41 -0
- package/dist/finalize/index.js +474 -0
- package/dist/finalize/linear.js +213 -0
- package/dist/finalize/run-report.js +47 -0
- package/dist/finalize/steps/01-map-update.js +11 -0
- package/dist/finalize/steps/02-map-validate.js +11 -0
- package/dist/finalize/steps/03-schema-validate.js +11 -0
- package/dist/finalize/steps/04-run-checks.js +118 -0
- package/dist/finalize/steps/05-generate-report.js +14 -0
- package/dist/finalize/steps/06-commit.js +39 -0
- package/dist/finalize/steps/07-push.js +10 -0
- package/dist/finalize/steps/08-create-pr.js +9 -0
- package/dist/finalize/steps/09-update-state.js +9 -0
- package/dist/finalize/steps/10-append-jsonl.js +23 -0
- package/dist/finalize/steps/11-update-linear.js +22 -0
- package/dist/finalize/steps/12-archive.js +21 -0
- package/dist/graph/adapter/c/adapter.js +45 -0
- package/dist/graph/adapter/c/extract.js +135 -0
- package/dist/graph/adapter/c/index.js +10 -0
- package/dist/graph/adapter/c/runtime.js +67 -0
- package/dist/graph/adapter/cpp/adapter.js +45 -0
- package/dist/graph/adapter/cpp/extract.js +159 -0
- package/dist/graph/adapter/cpp/index.js +10 -0
- package/dist/graph/adapter/cpp/runtime.js +61 -0
- package/dist/graph/adapter/csharp/adapter.js +45 -0
- package/dist/graph/adapter/csharp/extract.js +127 -0
- package/dist/graph/adapter/csharp/index.js +10 -0
- package/dist/graph/adapter/csharp/runtime.js +61 -0
- package/dist/graph/adapter/dart/adapter.js +56 -0
- package/dist/graph/adapter/dart/extract.js +130 -0
- package/dist/graph/adapter/dart/index.js +10 -0
- package/dist/graph/adapter/dart/runtime.js +61 -0
- package/dist/graph/adapter/go/adapter.js +45 -0
- package/dist/graph/adapter/go/extract.js +112 -0
- package/dist/graph/adapter/go/index.js +10 -0
- package/dist/graph/adapter/go/runtime.js +58 -0
- package/dist/graph/adapter/kotlin-java/extract-common.js +83 -0
- package/dist/graph/adapter/kotlin-java/index.js +15 -0
- package/dist/graph/adapter/kotlin-java/java-adapter.js +45 -0
- package/dist/graph/adapter/kotlin-java/java-extract.js +42 -0
- package/dist/graph/adapter/kotlin-java/kotlin-adapter.js +48 -0
- package/dist/graph/adapter/kotlin-java/kotlin-extract.js +58 -0
- package/dist/graph/adapter/kotlin-java/runtime.js +75 -0
- package/dist/graph/adapter/python/adapter.js +45 -0
- package/dist/graph/adapter/python/extract.js +121 -0
- package/dist/graph/adapter/python/index.js +10 -0
- package/dist/graph/adapter/python/runtime.js +70 -0
- package/dist/graph/adapter/registry.js +82 -0
- package/dist/graph/adapter/rust/adapter.js +45 -0
- package/dist/graph/adapter/rust/extract.js +110 -0
- package/dist/graph/adapter/rust/index.js +10 -0
- package/dist/graph/adapter/rust/runtime.js +74 -0
- package/dist/graph/adapter/shell/adapter.js +34 -0
- package/dist/graph/adapter/shell/extract.js +98 -0
- package/dist/graph/adapter/shell/index.js +8 -0
- package/dist/graph/adapter/svelte/adapter.js +91 -0
- package/dist/graph/adapter/svelte/extract.js +41 -0
- package/dist/graph/adapter/svelte/index.js +8 -0
- package/dist/graph/adapter/swift/adapter.js +45 -0
- package/dist/graph/adapter/swift/extract.js +120 -0
- package/dist/graph/adapter/swift/index.js +10 -0
- package/dist/graph/adapter/swift/runtime.js +70 -0
- package/dist/graph/adapter/types.js +2 -0
- package/dist/graph/adapter/typescript-javascript/adapter.js +72 -0
- package/dist/graph/adapter/typescript-javascript/extract.js +115 -0
- package/dist/graph/adapter/typescript-javascript/index.js +10 -0
- package/dist/graph/adapter/typescript-javascript/runtime.js +97 -0
- package/dist/graph/capability/index.js +105 -0
- package/dist/graph/governance.js +104 -0
- package/dist/graph/parser/extract.js +5 -0
- package/dist/graph/parser/loader.js +2 -0
- package/dist/graph/parser/pipeline.js +194 -0
- package/dist/graph/query/index.js +277 -0
- package/dist/graph/query/types.js +5 -0
- package/dist/graph/resolver/build-edges.js +211 -0
- package/dist/graph/resolver/index.js +23 -0
- package/dist/graph/resolver/resolve-imports.js +168 -0
- package/dist/graph/store/adapter.js +132 -0
- package/dist/graph/store/queries.js +124 -0
- package/dist/graph/store/types.js +4 -0
- package/dist/ignore/defaults.js +41 -0
- package/dist/ignore/parser.js +39 -0
- package/dist/lint/map-reference-validator.js +72 -0
- package/dist/loop/abort.js +188 -0
- package/dist/loop/abort.test.js +151 -0
- package/dist/loop/adapters/__tests__/terminal-cli.test.js +92 -0
- package/dist/loop/adapters/agent-subtask.js +137 -0
- package/dist/loop/adapters/agent-subtask.test.js +151 -0
- package/dist/loop/adapters/cli-subtask-bridge.js +108 -0
- package/dist/loop/adapters/index.js +9 -0
- package/dist/loop/adapters/registry.js +17 -0
- package/dist/loop/adapters/terminal-cli.js +403 -0
- package/dist/loop/adapters/types.js +2 -0
- package/dist/loop/adapters/worker-instructions.js +85 -0
- package/dist/loop/body-parser.js +226 -0
- package/dist/loop/bootstrap-packet.js +97 -0
- package/dist/loop/bootstrap-packet.test.js +171 -0
- package/dist/loop/branch-governance.js +54 -0
- package/dist/loop/budget.js +63 -0
- package/dist/loop/budget.test.js +163 -0
- package/dist/loop/checkpoint.js +325 -0
- package/dist/loop/compact-return.js +59 -0
- package/dist/loop/continue.js +645 -0
- package/dist/loop/continue.test.js +337 -0
- package/dist/loop/dispatch-boundary.js +335 -0
- package/dist/loop/dispatch-state.js +223 -0
- package/dist/loop/dispatch-state.test.js +186 -0
- package/dist/loop/dispatch.js +1236 -0
- package/dist/loop/dispatch.test.js +499 -0
- package/dist/loop/ephemeral-smoke.test.js +160 -0
- package/dist/loop/evidence-backfill.js +216 -0
- package/dist/loop/execution-adapter.js +141 -0
- package/dist/loop/execution-adapter.test.js +66 -0
- package/dist/loop/finalize-evidence.js +169 -0
- package/dist/loop/git-custody.js +250 -0
- package/dist/loop/impl-packet-input.js +38 -0
- package/dist/loop/index.js +270 -0
- package/dist/loop/ledger.js +49 -0
- package/dist/loop/ledger.test.js +127 -0
- package/dist/loop/lifecycle-dispatch.js +348 -0
- package/dist/loop/lifecycle-dispatch.test.js +318 -0
- package/dist/loop/lifecycle.js +93 -0
- package/dist/loop/lifecycle.test.js +150 -0
- package/dist/loop/orphan-recovery.js +276 -0
- package/dist/loop/orphan-recovery.test.js +236 -0
- package/dist/loop/parent.js +1566 -0
- package/dist/loop/parent.test.js +738 -0
- package/dist/loop/provider-sealed-packet.test.js +164 -0
- package/dist/loop/resume.js +295 -0
- package/dist/loop/resume.test.js +172 -0
- package/dist/loop/run-bootstrap.js +234 -0
- package/dist/loop/run-preflight.js +203 -0
- package/dist/loop/status.js +665 -0
- package/dist/loop/status.test.js +235 -0
- package/dist/loop/wait.js +86 -0
- package/dist/loop/worker-packet.js +406 -0
- package/dist/loop/worker-packet.test.js +243 -0
- package/dist/loop/worker-prompt.js +260 -0
- package/dist/loop/worker-prompt.test.js +247 -0
- package/dist/loop/worker.js +498 -0
- package/dist/loop/worker.test.js +372 -0
- package/dist/map/atlas.js +77 -0
- package/dist/map/backfill.js +171 -0
- package/dist/map/backfill.test.js +120 -0
- package/dist/map/index.js +255 -0
- package/dist/map/inference.js +73 -0
- package/dist/map/inference.test.js +35 -0
- package/dist/map/query.js +157 -0
- package/dist/map/query.test.js +174 -0
- package/dist/map/update.js +355 -0
- package/dist/map/update.test.js +67 -0
- package/dist/map/validate.js +153 -0
- package/dist/map/validate.test.js +90 -0
- package/dist/map/welfare.js +86 -0
- package/dist/mcp/lib/invoke.js +35 -0
- package/dist/mcp/lib/redact.js +24 -0
- package/dist/mcp/lib/redact.test.js +112 -0
- package/dist/mcp/lib/root.js +41 -0
- package/dist/mcp/lib/root.test.js +114 -0
- package/dist/mcp/server.js +15 -0
- package/dist/mcp/tools/claim-child.js +119 -0
- package/dist/mcp/tools/claim-child.test.js +69 -0
- package/dist/mcp/tools/current-state.js +73 -0
- package/dist/mcp/tools/current-state.test.js +114 -0
- package/dist/mcp/tools/dispatch-result.js +186 -0
- package/dist/mcp/tools/dispatch-result.test.js +109 -0
- package/dist/mcp/tools/index.js +222 -0
- package/dist/mcp/tools/index.test.js +172 -0
- package/dist/mcp/tools/linear.js +74 -0
- package/dist/mcp/tools/loop-continue.js +159 -0
- package/dist/mcp/tools/loop-continue.test.js +186 -0
- package/dist/mcp/tools/loop-dry-run.js +15 -0
- package/dist/mcp/tools/status.js +65 -0
- package/dist/medic/chart-id.js +75 -0
- package/dist/medic/chart-schema.js +139 -0
- package/dist/runs/index.js +119 -0
- package/dist/runs/index.test.js +184 -0
- package/dist/runs/ledger-tail.js +15 -0
- package/dist/runs/list.js +29 -0
- package/dist/runs/reconcile.js +74 -0
- package/dist/runs/show.js +10 -0
- package/dist/runtime/audit/logger.js +15 -0
- package/dist/runtime/audit/reader.js +42 -0
- package/dist/runtime/checkpoint.js +89 -0
- package/dist/runtime/continuation/confirmed.js +225 -0
- package/dist/runtime/continuation/confirmed.test.js +421 -0
- package/dist/runtime/continuation/dry-run.js +74 -0
- package/dist/runtime/execution-window.js +106 -0
- package/dist/runtime/scheduling/child-selector.js +16 -0
- package/dist/runtime/state.js +43 -0
- package/dist/runtime/status.js +46 -0
- package/dist/runtime/verification/envelope.js +80 -0
- package/dist/runtime/verification/fingerprint.js +26 -0
- package/dist/skill-packet/cli.test.js +105 -0
- package/dist/skill-packet/generator.js +225 -0
- package/dist/skill-packet/generator.test.js +159 -0
- package/dist/skill-packet/index.js +49 -0
- package/dist/skill-packet/types.js +2 -0
- package/dist/smartdocs-engine/audit.js +194 -0
- package/dist/smartdocs-engine/audit.test.js +122 -0
- package/dist/smartdocs-engine/canon-check.js +340 -0
- package/dist/smartdocs-engine/canon-check.test.js +234 -0
- package/dist/smartdocs-engine/doctrine.js +633 -0
- package/dist/smartdocs-engine/doctrine.test.js +374 -0
- package/dist/smartdocs-engine/index.js +456 -0
- package/dist/smartdocs-engine/index.test.js +82 -0
- package/dist/smartdocs-engine/ingest.js +495 -0
- package/dist/smartdocs-engine/ingest.test.js +163 -0
- package/dist/smartdocs-engine/migrate.js +217 -0
- package/dist/smartdocs-engine/migrate.test.js +200 -0
- package/dist/smartdocs-engine/seed-instructions.js +335 -0
- package/dist/smartdocs-engine/seed-instructions.test.js +275 -0
- package/dist/smartdocs-engine/smartdoc-ignore.js +258 -0
- package/dist/smartdocs-engine/smartdoc-ignore.test.js +146 -0
- package/dist/smartdocs-engine/validate-instructions.js +366 -0
- package/dist/smartdocs-engine/validate-instructions.test.js +283 -0
- package/dist/tracker/adapters/github/index.js +305 -0
- package/dist/tracker/adapters/jira/index.js +293 -0
- package/dist/tracker/adapters/linear/http-errors.test.js +51 -0
- package/dist/tracker/adapters/linear/index.js +621 -0
- package/dist/tracker/adapters/linear/index.test.js +238 -0
- package/dist/tracker/adapters/linear.js +50 -0
- package/dist/tracker/adapters/local/index.js +10 -0
- package/dist/tracker/adapters/local-file/index.js +159 -0
- package/dist/tracker/adapters/mcp-bridge.js +158 -0
- package/dist/tracker/adapters/mcp-bridge.test.js +12 -0
- package/dist/tracker/adapters/spec/index.js +146 -0
- package/dist/tracker/capabilities.js +2 -0
- package/dist/tracker/index.js +49 -0
- package/dist/tracker/lifecycle-policy.js +139 -0
- package/dist/tracker/lifecycle-transition.js +164 -0
- package/dist/tracker/local-graph.js +98 -0
- package/dist/tracker/migration.js +56 -0
- package/dist/tracker/schema.js +86 -0
- package/dist/tracker/sync/index.js +404 -0
- package/dist/tracker/sync/queue-store.js +95 -0
- package/dist/tracker/sync/queue-store.test.js +104 -0
- package/dist/tracker/types.js +11 -0
- package/dist/types/linear.js +3 -0
- package/dist/types/result-packet.js +12 -0
- package/dist/types/runtime-state.js +2 -0
- package/dist/types/work-contract.js +2 -0
- package/dist/utils/monotonic-timestamp.js +13 -0
- package/dist/utils/monotonic-timestamp.test.js +19 -0
- package/package.json +64 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CANDIDATE_MARKER = void 0;
|
|
4
|
+
exports.parseFrontMatter = parseFrontMatter;
|
|
5
|
+
exports.addCandidateGovernanceMetadata = addCandidateGovernanceMetadata;
|
|
6
|
+
exports.stampIngestFrontMatter = stampIngestFrontMatter;
|
|
7
|
+
exports.doctrineDraft = doctrineDraft;
|
|
8
|
+
exports.doctrinePromote = doctrinePromote;
|
|
9
|
+
exports.doctrineDeprecate = doctrineDeprecate;
|
|
10
|
+
exports.specPromote = specPromote;
|
|
11
|
+
exports.migrateProvenance = migrateProvenance;
|
|
12
|
+
const node_fs_1 = require("node:fs");
|
|
13
|
+
const node_path_1 = require("node:path");
|
|
14
|
+
exports.CANDIDATE_MARKER = "<!-- polaris:doctrine-candidate -->";
|
|
15
|
+
function generateRunId() {
|
|
16
|
+
return `polaris-doctrine-${new Date().toISOString().slice(0, 10)}-001`;
|
|
17
|
+
}
|
|
18
|
+
function lifecycleFilePath(repoRoot, runId) {
|
|
19
|
+
return (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-doctrine", runId, "lifecycle.jsonl");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Builds the filesystem path to the audit JSONL file for a given doctrine run.
|
|
23
|
+
*
|
|
24
|
+
* @param repoRoot - The repository root directory
|
|
25
|
+
* @param runId - The doctrine run identifier
|
|
26
|
+
* @returns The full path to `.taskchain_artifacts/polaris-doctrine/<runId>/audit.jsonl`
|
|
27
|
+
*/
|
|
28
|
+
function auditFilePath(repoRoot, runId) {
|
|
29
|
+
return (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-doctrine", runId, "audit.jsonl");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Parse YAML-style frontmatter from a Markdown string into a map of raw key/value pairs.
|
|
33
|
+
*
|
|
34
|
+
* Normalizes CRLF to LF and tolerates a leading candidate marker line. If a frontmatter
|
|
35
|
+
* block delimited by `---` is found, each line inside the block is split at the first
|
|
36
|
+
* `:`; keys are lowercased and trimmed, values are trimmed and stripped of surrounding
|
|
37
|
+
* single/double quotes. If no well-formed frontmatter is present, an empty map is returned.
|
|
38
|
+
*
|
|
39
|
+
* @param content - The raw file content to inspect for a frontmatter block.
|
|
40
|
+
* @returns A Map where each entry is `key -> value` from the frontmatter; keys are lowercased and values are trimmed with surrounding quotes removed.
|
|
41
|
+
*/
|
|
42
|
+
function parseFrontMatterRaw(content) {
|
|
43
|
+
const result = new Map();
|
|
44
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
45
|
+
const stripped = normalized.startsWith(exports.CANDIDATE_MARKER)
|
|
46
|
+
? normalized.slice(exports.CANDIDATE_MARKER.length).replace(/^\n/, "")
|
|
47
|
+
: normalized;
|
|
48
|
+
if (!stripped.startsWith("---\n"))
|
|
49
|
+
return result;
|
|
50
|
+
const end = stripped.indexOf("\n---", 4);
|
|
51
|
+
if (end === -1)
|
|
52
|
+
return result;
|
|
53
|
+
const lines = stripped.slice(4, end).split("\n");
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
const colonIdx = line.indexOf(":");
|
|
56
|
+
if (colonIdx === -1)
|
|
57
|
+
continue;
|
|
58
|
+
const key = line.slice(0, colonIdx).trim().toLowerCase();
|
|
59
|
+
const value = line.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, "");
|
|
60
|
+
result.set(key, value);
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Extracts YAML-style front matter from markdown content into a ParsedFrontMatter object.
|
|
66
|
+
*
|
|
67
|
+
* Strips a leading candidate marker line if present and parses key:value pairs from a YAML-style block delimited by `---`.
|
|
68
|
+
*
|
|
69
|
+
* @param content - Markdown file content to parse
|
|
70
|
+
* @returns The parsed front matter as a ParsedFrontMatter object where keys are front-matter fields and values are their string values
|
|
71
|
+
*/
|
|
72
|
+
function parseFrontMatter(content) {
|
|
73
|
+
const raw = parseFrontMatterRaw(content);
|
|
74
|
+
const result = {};
|
|
75
|
+
for (const [key, value] of raw) {
|
|
76
|
+
result[key] = value;
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Ensure a candidate governance frontmatter block exists and add placeholder governance and relationship fields without overwriting existing keys.
|
|
82
|
+
*
|
|
83
|
+
* Normalizes CRLF to LF. If a YAML-style frontmatter block is present it appends any missing governance keys; if no frontmatter exists it prepends a new block containing the default governance and relationship scaffold. Existing frontmatter keys and their values are preserved.
|
|
84
|
+
*
|
|
85
|
+
* @param content - The markdown document content to update
|
|
86
|
+
* @param docType - The value to use for the `doc-type` field in the frontmatter
|
|
87
|
+
* @returns The markdown content with governance placeholder frontmatter added or updated
|
|
88
|
+
*/
|
|
89
|
+
function addCandidateGovernanceMetadata(content, docType) {
|
|
90
|
+
const govDefaults = {
|
|
91
|
+
"doc-type": docType,
|
|
92
|
+
"confidence": "0.0",
|
|
93
|
+
"recommended-action": "hold",
|
|
94
|
+
"overlap-analysis": "pending",
|
|
95
|
+
// Relationship scaffolding — populated by author before promotion
|
|
96
|
+
"implements": "",
|
|
97
|
+
"related": "",
|
|
98
|
+
"supersedes": "",
|
|
99
|
+
"superseded_by": "",
|
|
100
|
+
"depends_on": "",
|
|
101
|
+
"validates": "",
|
|
102
|
+
"source_paths": "",
|
|
103
|
+
};
|
|
104
|
+
// Normalize line endings to Unix style
|
|
105
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
106
|
+
if (normalized.startsWith("---\n")) {
|
|
107
|
+
const end = normalized.indexOf("\n---", 4);
|
|
108
|
+
if (end !== -1) {
|
|
109
|
+
const frontMatter = normalized.slice(4, end);
|
|
110
|
+
const afterFrontMatter = normalized.slice(end + 4);
|
|
111
|
+
const lines = frontMatter.split("\n");
|
|
112
|
+
const existingKeys = new Set(lines
|
|
113
|
+
.filter((l) => l.includes(":"))
|
|
114
|
+
.map((l) => l.slice(0, l.indexOf(":")).trim().toLowerCase()));
|
|
115
|
+
const additions = [];
|
|
116
|
+
for (const [key, val] of Object.entries(govDefaults)) {
|
|
117
|
+
if (!existingKeys.has(key))
|
|
118
|
+
additions.push(`${key}: ${val}`);
|
|
119
|
+
}
|
|
120
|
+
if (additions.length === 0)
|
|
121
|
+
return normalized;
|
|
122
|
+
return `---\n${frontMatter}\n${additions.join("\n")}\n---${afterFrontMatter}`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// No front matter — create one
|
|
126
|
+
const fields = Object.entries(govDefaults)
|
|
127
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
128
|
+
.join("\n");
|
|
129
|
+
return `---\n${fields}\n---\n\n${normalized}`;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Stamp ingest provenance directly into a SmartDoc's frontmatter.
|
|
133
|
+
*
|
|
134
|
+
* Instead of writing a separate `.provenance.json` sidecar, this writes
|
|
135
|
+
* the same metadata into the file's own YAML frontmatter using canonical
|
|
136
|
+
* schema fields. Existing frontmatter keys are never overwritten.
|
|
137
|
+
*
|
|
138
|
+
* Fields written (only if not already present):
|
|
139
|
+
* - `source` ← originalPath
|
|
140
|
+
* - `ingest-run-id` ← ingestRunId
|
|
141
|
+
* - `ingest-cluster` ← ingestClusterId
|
|
142
|
+
* - `classified-as` ← classifiedAs
|
|
143
|
+
* - `linked-map-area` ← linkedMapArea
|
|
144
|
+
* - `ingested-at` ← ISO timestamp
|
|
145
|
+
* - `status` ← "raw" (if not already set)
|
|
146
|
+
*
|
|
147
|
+
* @param content - The current file content (will be read from disk by caller)
|
|
148
|
+
* @param stamp - Ingest metadata to embed
|
|
149
|
+
* @returns The updated file content with provenance stamped in frontmatter
|
|
150
|
+
*/
|
|
151
|
+
function stampIngestFrontMatter(content, stamp) {
|
|
152
|
+
const fields = {
|
|
153
|
+
source: stamp.originalPath,
|
|
154
|
+
"ingest-run-id": stamp.ingestRunId,
|
|
155
|
+
...(stamp.ingestClusterId ? { "ingest-cluster": stamp.ingestClusterId } : {}),
|
|
156
|
+
"classified-as": stamp.classifiedAs,
|
|
157
|
+
...(stamp.linkedMapArea ? { "linked-map-area": stamp.linkedMapArea } : {}),
|
|
158
|
+
"ingested-at": stamp.ingestedAt,
|
|
159
|
+
status: "raw",
|
|
160
|
+
};
|
|
161
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
162
|
+
if (normalized.startsWith("---\n")) {
|
|
163
|
+
const end = normalized.indexOf("\n---", 4);
|
|
164
|
+
if (end !== -1) {
|
|
165
|
+
const frontMatter = normalized.slice(4, end);
|
|
166
|
+
const afterFrontMatter = normalized.slice(end + 4);
|
|
167
|
+
const existingKeys = new Set(frontMatter
|
|
168
|
+
.split("\n")
|
|
169
|
+
.filter((l) => l.includes(":"))
|
|
170
|
+
.map((l) => l.slice(0, l.indexOf(":")).trim().toLowerCase()));
|
|
171
|
+
const additions = [];
|
|
172
|
+
for (const [key, val] of Object.entries(fields)) {
|
|
173
|
+
if (!existingKeys.has(key.toLowerCase()))
|
|
174
|
+
additions.push(`${key}: ${val}`);
|
|
175
|
+
}
|
|
176
|
+
if (additions.length === 0)
|
|
177
|
+
return normalized;
|
|
178
|
+
return `---\n${frontMatter}\n${additions.join("\n")}\n---${afterFrontMatter}`;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// No frontmatter — create one
|
|
182
|
+
const block = Object.entries(fields).map(([k, v]) => `${k}: ${v}`).join("\n");
|
|
183
|
+
return `---\n${block}\n---\n\n${normalized}`;
|
|
184
|
+
}
|
|
185
|
+
function appendLifecycle(lifecyclePath, event) {
|
|
186
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(lifecyclePath), { recursive: true });
|
|
187
|
+
(0, node_fs_1.appendFileSync)(lifecyclePath, JSON.stringify(event) + "\n", "utf-8");
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Resolve a filesystem path against a repository root and return an absolute path.
|
|
191
|
+
*
|
|
192
|
+
* @param path - The input path to resolve; may be absolute or relative.
|
|
193
|
+
* @param repoRoot - The repository root used to resolve relative paths.
|
|
194
|
+
* @returns An absolute filesystem path. If `path` starts with `/` it is returned unchanged; otherwise the result of resolving `path` against `repoRoot`.
|
|
195
|
+
*/
|
|
196
|
+
function resolvePath(path, repoRoot) {
|
|
197
|
+
if (path.startsWith("/"))
|
|
198
|
+
return path;
|
|
199
|
+
return (0, node_path_1.join)((0, node_path_1.resolve)(repoRoot), path);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Move a markdown file from the repository's smartdocs/raw/ directory into smartdocs/doctrine/candidate/,
|
|
203
|
+
* mark it as a doctrine candidate, delete the original, and append a lifecycle event.
|
|
204
|
+
*
|
|
205
|
+
* @param path - Path to the source file; if not absolute, it is resolved relative to `options.repoRoot`
|
|
206
|
+
* @param options - Operation options; `options.repoRoot` specifies the repository root and `options.runId` may override the generated run id
|
|
207
|
+
* @returns An object containing `source` (resolved source path), `destination` (path in the candidate directory), `runId` (the run identifier used), and `lifecyclePath` (path to the lifecycle log file)
|
|
208
|
+
*/
|
|
209
|
+
function doctrineDraft(path, options) {
|
|
210
|
+
const repoRoot = (0, node_path_1.resolve)(options.repoRoot);
|
|
211
|
+
const runId = options.runId ?? generateRunId();
|
|
212
|
+
const source = resolvePath(path, repoRoot);
|
|
213
|
+
if (!(0, node_fs_1.existsSync)(source)) {
|
|
214
|
+
throw new Error(`Source file not found: ${source}`);
|
|
215
|
+
}
|
|
216
|
+
const rawDir = (0, node_path_1.resolve)(repoRoot, "smartdocs", "raw");
|
|
217
|
+
const relToRaw = (0, node_path_1.relative)(rawDir, source);
|
|
218
|
+
const isInRaw = !relToRaw.startsWith("..") && !relToRaw.startsWith("/");
|
|
219
|
+
if (!isInRaw) {
|
|
220
|
+
throw new Error(`doctrineDraft source must be in smartdocs/raw/ — got: ${source}`);
|
|
221
|
+
}
|
|
222
|
+
const candidateDir = (0, node_path_1.join)(repoRoot, "smartdocs", "doctrine", "candidate");
|
|
223
|
+
(0, node_fs_1.mkdirSync)(candidateDir, { recursive: true });
|
|
224
|
+
const destination = (0, node_path_1.join)(candidateDir, (0, node_path_1.basename)(source));
|
|
225
|
+
if ((0, node_fs_1.existsSync)(destination)) {
|
|
226
|
+
throw new Error(`Destination already exists: ${destination}`);
|
|
227
|
+
}
|
|
228
|
+
const content = (0, node_fs_1.readFileSync)(source, "utf-8");
|
|
229
|
+
(0, node_fs_1.writeFileSync)(destination, `${exports.CANDIDATE_MARKER}\n${content}`, "utf-8");
|
|
230
|
+
(0, node_fs_1.unlinkSync)(source);
|
|
231
|
+
const lifecyclePath = lifecycleFilePath(repoRoot, runId);
|
|
232
|
+
appendLifecycle(lifecyclePath, {
|
|
233
|
+
event: "doctrine-draft",
|
|
234
|
+
run_id: runId,
|
|
235
|
+
source,
|
|
236
|
+
destination,
|
|
237
|
+
timestamp: new Date().toISOString(),
|
|
238
|
+
});
|
|
239
|
+
return { source, destination, runId, lifecyclePath };
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Promote a candidate SmartDoc into the doctrine active pool.
|
|
243
|
+
*
|
|
244
|
+
* Moves a Markdown file from smartdocs/doctrine/candidate/ to smartdocs/doctrine/active/,
|
|
245
|
+
* removes the candidate marker, preserves a provenance sidecar if present, records an audit
|
|
246
|
+
* entry, and appends a lifecycle event.
|
|
247
|
+
*
|
|
248
|
+
* @param path - Path to the candidate Markdown file (absolute or repository-relative)
|
|
249
|
+
* @param options - Doctrine operation options (must include `repoRoot`; may include `runId`)
|
|
250
|
+
* @returns The operation result containing `source`, `destination`, `runId`, and `lifecyclePath`
|
|
251
|
+
* @throws If the source file does not exist
|
|
252
|
+
* @throws If the source is not located under smartdocs/doctrine/candidate/
|
|
253
|
+
* @throws If the file does not contain the candidate marker
|
|
254
|
+
* @throws If required governance frontmatter fields are missing
|
|
255
|
+
* @throws If `recommended-action` in frontmatter is not `"promote"`
|
|
256
|
+
* @throws If the destination file already exists
|
|
257
|
+
*/
|
|
258
|
+
function doctrinePromote(path, options) {
|
|
259
|
+
const repoRoot = (0, node_path_1.resolve)(options.repoRoot);
|
|
260
|
+
const runId = options.runId ?? generateRunId();
|
|
261
|
+
const source = resolvePath(path, repoRoot);
|
|
262
|
+
if (!(0, node_fs_1.existsSync)(source)) {
|
|
263
|
+
throw new Error(`Source file not found: ${source}`);
|
|
264
|
+
}
|
|
265
|
+
const candidateDir = (0, node_path_1.resolve)(repoRoot, "smartdocs", "doctrine", "candidate");
|
|
266
|
+
const relToCandidate = (0, node_path_1.relative)(candidateDir, source);
|
|
267
|
+
const isInCandidate = !relToCandidate.startsWith("..") && !relToCandidate.startsWith("/");
|
|
268
|
+
if (!isInCandidate) {
|
|
269
|
+
throw new Error(`doctrinePromote source must be in smartdocs/doctrine/candidate/ — got: ${source}`);
|
|
270
|
+
}
|
|
271
|
+
const content = (0, node_fs_1.readFileSync)(source, "utf-8");
|
|
272
|
+
if (!content.includes(exports.CANDIDATE_MARKER)) {
|
|
273
|
+
throw new Error(`File is not in candidate state (missing ${exports.CANDIDATE_MARKER}): ${source}`);
|
|
274
|
+
}
|
|
275
|
+
const lifecyclePath = lifecycleFilePath(repoRoot, runId);
|
|
276
|
+
// Governance check
|
|
277
|
+
const fm = parseFrontMatterRaw(content);
|
|
278
|
+
const requiredFields = ["doc-type", "confidence", "recommended-action", "overlap-analysis"];
|
|
279
|
+
for (const field of requiredFields) {
|
|
280
|
+
if (!fm.has(field)) {
|
|
281
|
+
throw new Error(`doctrinePromote: missing required governance field "${field}" in ${source}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const recommendedAction = fm.get("recommended-action");
|
|
285
|
+
if (recommendedAction !== "promote") {
|
|
286
|
+
throw new Error(`doctrinePromote: recommended-action must be "promote" but got "${recommendedAction}" in ${source}`);
|
|
287
|
+
}
|
|
288
|
+
const activeDir = (0, node_path_1.join)(repoRoot, "smartdocs", "doctrine", "active");
|
|
289
|
+
(0, node_fs_1.mkdirSync)(activeDir, { recursive: true });
|
|
290
|
+
const destination = (0, node_path_1.join)(activeDir, (0, node_path_1.basename)(source));
|
|
291
|
+
if ((0, node_fs_1.existsSync)(destination)) {
|
|
292
|
+
throw new Error(`Destination already exists: ${destination}`);
|
|
293
|
+
}
|
|
294
|
+
const activeContent = content.replace(`${exports.CANDIDATE_MARKER}\n`, "").replace(exports.CANDIDATE_MARKER, "");
|
|
295
|
+
(0, node_fs_1.writeFileSync)(destination, activeContent, "utf-8");
|
|
296
|
+
(0, node_fs_1.unlinkSync)(source);
|
|
297
|
+
// Move co-located provenance sidecar if present
|
|
298
|
+
const provenanceSrc = source.replace(/\.md$/, ".provenance.json");
|
|
299
|
+
if ((0, node_fs_1.existsSync)(provenanceSrc)) {
|
|
300
|
+
const provenanceDest = destination.replace(/\.md$/, ".provenance.json");
|
|
301
|
+
(0, node_fs_1.renameSync)(provenanceSrc, provenanceDest);
|
|
302
|
+
}
|
|
303
|
+
// Write audit record
|
|
304
|
+
const auditPath = auditFilePath(repoRoot, runId);
|
|
305
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(auditPath), { recursive: true });
|
|
306
|
+
(0, node_fs_1.appendFileSync)(auditPath, JSON.stringify({
|
|
307
|
+
event: "doctrine-promoted",
|
|
308
|
+
run_id: runId,
|
|
309
|
+
source,
|
|
310
|
+
destination,
|
|
311
|
+
doc_type: fm.get("doc-type") ?? null,
|
|
312
|
+
confidence: fm.has("confidence") ? parseFloat(fm.get("confidence")) : null,
|
|
313
|
+
recommended_action: fm.get("recommended-action") ?? null,
|
|
314
|
+
overlap_analysis: fm.get("overlap-analysis") ?? null,
|
|
315
|
+
promoted_by: "polaris-cli",
|
|
316
|
+
timestamp: new Date().toISOString(),
|
|
317
|
+
}) + "\n", "utf-8");
|
|
318
|
+
appendLifecycle(lifecyclePath, {
|
|
319
|
+
event: "doctrine-promote",
|
|
320
|
+
run_id: runId,
|
|
321
|
+
source,
|
|
322
|
+
destination,
|
|
323
|
+
timestamp: new Date().toISOString(),
|
|
324
|
+
});
|
|
325
|
+
return { source, destination, runId, lifecyclePath };
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Move a doctrine document from smartdocs/doctrine/active/ into smartdocs/doctrine/deprecated/ and record the deprecation event.
|
|
329
|
+
*
|
|
330
|
+
* @param path - Absolute or repository-relative path to the source markdown file; must be located in smartdocs/doctrine/active/.
|
|
331
|
+
* @param options - Options object containing `repoRoot` (repository root used to resolve relative paths) and an optional `runId`.
|
|
332
|
+
* @returns An object with `source` (original path), `destination` (new path under deprecated), `runId` used for this operation, and `lifecyclePath` where the lifecycle event was appended.
|
|
333
|
+
* @throws Error if the source file does not exist, if the source is not inside smartdocs/doctrine/active/, or if the destination file already exists.
|
|
334
|
+
*/
|
|
335
|
+
function doctrineDeprecate(path, options) {
|
|
336
|
+
const repoRoot = (0, node_path_1.resolve)(options.repoRoot);
|
|
337
|
+
const runId = options.runId ?? generateRunId();
|
|
338
|
+
const source = resolvePath(path, repoRoot);
|
|
339
|
+
if (!(0, node_fs_1.existsSync)(source)) {
|
|
340
|
+
throw new Error(`Source file not found: ${source}`);
|
|
341
|
+
}
|
|
342
|
+
const activeDir = (0, node_path_1.resolve)(repoRoot, "smartdocs", "doctrine", "active");
|
|
343
|
+
const relToActive = (0, node_path_1.relative)(activeDir, source);
|
|
344
|
+
const isInActive = !relToActive.startsWith("..") && !relToActive.startsWith("/");
|
|
345
|
+
if (!isInActive) {
|
|
346
|
+
throw new Error(`doctrineDeprecate source must be in smartdocs/doctrine/active/ — got: ${source}`);
|
|
347
|
+
}
|
|
348
|
+
const deprecatedDir = (0, node_path_1.join)(repoRoot, "smartdocs", "doctrine", "deprecated");
|
|
349
|
+
(0, node_fs_1.mkdirSync)(deprecatedDir, { recursive: true });
|
|
350
|
+
const destination = (0, node_path_1.join)(deprecatedDir, (0, node_path_1.basename)(source));
|
|
351
|
+
if ((0, node_fs_1.existsSync)(destination)) {
|
|
352
|
+
throw new Error(`Destination already exists: ${destination}`);
|
|
353
|
+
}
|
|
354
|
+
const content = (0, node_fs_1.readFileSync)(source, "utf-8");
|
|
355
|
+
const deprecatedAt = new Date().toISOString();
|
|
356
|
+
const deprecatedContent = `<!-- polaris:doctrine-deprecated deprecatedAt="${deprecatedAt}" runId="${runId}" -->\n${content}`;
|
|
357
|
+
(0, node_fs_1.writeFileSync)(destination, deprecatedContent, "utf-8");
|
|
358
|
+
(0, node_fs_1.unlinkSync)(source);
|
|
359
|
+
// Move co-located provenance sidecar if present
|
|
360
|
+
const provenanceSrc = source.replace(/\.md$/, ".provenance.json");
|
|
361
|
+
if ((0, node_fs_1.existsSync)(provenanceSrc)) {
|
|
362
|
+
const provenanceDest = destination.replace(/\.md$/, ".provenance.json");
|
|
363
|
+
(0, node_fs_1.renameSync)(provenanceSrc, provenanceDest);
|
|
364
|
+
}
|
|
365
|
+
const lifecyclePath = lifecycleFilePath(repoRoot, runId);
|
|
366
|
+
appendLifecycle(lifecyclePath, {
|
|
367
|
+
event: "doctrine-deprecate",
|
|
368
|
+
run_id: runId,
|
|
369
|
+
source,
|
|
370
|
+
destination,
|
|
371
|
+
deprecated_at: deprecatedAt,
|
|
372
|
+
timestamp: deprecatedAt,
|
|
373
|
+
});
|
|
374
|
+
return { source, destination, runId, lifecyclePath };
|
|
375
|
+
}
|
|
376
|
+
// ── Spec verb-keyword conflict detection ──────────────────────────────────────
|
|
377
|
+
const MODAL_REQUIRES = /\b(?:must\s+always|always|must)\s+(\w+)/gi;
|
|
378
|
+
const MODAL_PROHIBITS = /\b(?:must\s+never|must\s+not|never)\s+(\w+)/gi;
|
|
379
|
+
const CONFLICT_STOPWORDS = new Set([
|
|
380
|
+
"this", "that", "with", "from", "have", "been", "will", "when", "then",
|
|
381
|
+
"also", "only", "into", "over", "such", "each", "more", "same", "used",
|
|
382
|
+
]);
|
|
383
|
+
function extractSpecKeywords(content, pattern) {
|
|
384
|
+
const result = new Set();
|
|
385
|
+
const re = new RegExp(pattern.source, pattern.flags);
|
|
386
|
+
for (const match of content.matchAll(re)) {
|
|
387
|
+
const kw = match[1]?.toLowerCase();
|
|
388
|
+
if (kw && kw.length >= 4 && !CONFLICT_STOPWORDS.has(kw))
|
|
389
|
+
result.add(kw);
|
|
390
|
+
}
|
|
391
|
+
return result;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Promotes a spec file from smartdocs/raw/ into smartdocs/specs/active/.
|
|
395
|
+
*
|
|
396
|
+
* Performs two pre-promotion checks: content conflicts against existing active specs
|
|
397
|
+
* (verb-keyword requirements vs prohibitions) and map-area conflicts based on a
|
|
398
|
+
* provenance `linkedMapArea`. If conflicts are found the operation halts and returns
|
|
399
|
+
* a human-readable report unless approval is explicitly provided.
|
|
400
|
+
*
|
|
401
|
+
* @param path - Filesystem path to the source `.md` spec (must reside under `smartdocs/raw/`)
|
|
402
|
+
* @param options - Promotion options and environment:
|
|
403
|
+
* - `repoRoot`: repository root directory
|
|
404
|
+
* - `approve`: when true, proceed with promotion despite detected conflicts
|
|
405
|
+
* - `runId`: optional run identifier to use for lifecycle/audit paths
|
|
406
|
+
* @returns The promotion result containing:
|
|
407
|
+
* - `source`: original source path
|
|
408
|
+
* - `destination`: destination path under `smartdocs/specs/active/` (empty string if halted)
|
|
409
|
+
* - `runId`: run identifier used
|
|
410
|
+
* - `lifecyclePath`: path to the lifecycle log for this run
|
|
411
|
+
* - `conflicts`: array of detected `SpecConflict` entries
|
|
412
|
+
* - `halted`: `true` if promotion was stopped due to conflicts and `approve` was not set
|
|
413
|
+
* - `report`: multi-line text report summarizing detected conflicts and action taken
|
|
414
|
+
*/
|
|
415
|
+
function specPromote(path, options) {
|
|
416
|
+
const repoRoot = (0, node_path_1.resolve)(options.repoRoot);
|
|
417
|
+
const runId = options.runId ?? generateRunId();
|
|
418
|
+
const source = resolvePath(path, repoRoot);
|
|
419
|
+
const lifecyclePath = lifecycleFilePath(repoRoot, runId);
|
|
420
|
+
if (!(0, node_fs_1.existsSync)(source)) {
|
|
421
|
+
throw new Error(`Source file not found: ${source}`);
|
|
422
|
+
}
|
|
423
|
+
const rawDir = (0, node_path_1.resolve)(repoRoot, "smartdocs", "raw");
|
|
424
|
+
const relToRaw = (0, node_path_1.relative)(rawDir, source);
|
|
425
|
+
const isInRaw = !relToRaw.startsWith("..") && !relToRaw.startsWith("/");
|
|
426
|
+
if (!isInRaw) {
|
|
427
|
+
throw new Error(`specPromote source must be in smartdocs/raw/ — got: ${source}`);
|
|
428
|
+
}
|
|
429
|
+
const content = (0, node_fs_1.readFileSync)(source, "utf-8");
|
|
430
|
+
const conflicts = [];
|
|
431
|
+
// 1. Content conflict check against specs/active/
|
|
432
|
+
const activeSpecsDir = (0, node_path_1.resolve)(repoRoot, "smartdocs", "specs", "active");
|
|
433
|
+
if ((0, node_fs_1.existsSync)(activeSpecsDir)) {
|
|
434
|
+
const activeFiles = (0, node_fs_1.readdirSync)(activeSpecsDir).filter((f) => f.endsWith(".md"));
|
|
435
|
+
const incomingRequires = extractSpecKeywords(content, MODAL_REQUIRES);
|
|
436
|
+
const incomingProhibits = extractSpecKeywords(content, MODAL_PROHIBITS);
|
|
437
|
+
for (const file of activeFiles) {
|
|
438
|
+
let activeContent;
|
|
439
|
+
try {
|
|
440
|
+
activeContent = (0, node_fs_1.readFileSync)((0, node_path_1.join)(activeSpecsDir, file), "utf-8");
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const activeRequires = extractSpecKeywords(activeContent, MODAL_REQUIRES);
|
|
446
|
+
const activeProhibits = extractSpecKeywords(activeContent, MODAL_PROHIBITS);
|
|
447
|
+
for (const kw of incomingProhibits) {
|
|
448
|
+
if (activeRequires.has(kw)) {
|
|
449
|
+
conflicts.push({ type: "content", conflictingFile: file, detail: `incoming doc prohibits "${kw}" but ${file} requires it` });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
for (const kw of incomingRequires) {
|
|
453
|
+
if (activeProhibits.has(kw)) {
|
|
454
|
+
conflicts.push({ type: "content", conflictingFile: file, detail: `incoming doc requires "${kw}" but ${file} prohibits it` });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// 2. Map conflict check — linkedMapArea already has an active spec
|
|
460
|
+
const provenanceSrcPath = source.replace(/\.md$/, ".provenance.json");
|
|
461
|
+
let linkedMapArea = null;
|
|
462
|
+
if ((0, node_fs_1.existsSync)(provenanceSrcPath)) {
|
|
463
|
+
try {
|
|
464
|
+
const prov = JSON.parse((0, node_fs_1.readFileSync)(provenanceSrcPath, "utf-8"));
|
|
465
|
+
linkedMapArea = prov.linkedMapArea ?? null;
|
|
466
|
+
}
|
|
467
|
+
catch { /* ignore */ }
|
|
468
|
+
}
|
|
469
|
+
if (linkedMapArea && (0, node_fs_1.existsSync)(activeSpecsDir)) {
|
|
470
|
+
const activeFiles = (0, node_fs_1.readdirSync)(activeSpecsDir).filter((f) => f.endsWith(".md"));
|
|
471
|
+
for (const file of activeFiles) {
|
|
472
|
+
let activeContent;
|
|
473
|
+
try {
|
|
474
|
+
activeContent = (0, node_fs_1.readFileSync)((0, node_path_1.join)(activeSpecsDir, file), "utf-8");
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (activeContent.includes(linkedMapArea)) {
|
|
480
|
+
conflicts.push({ type: "map", conflictingFile: file, detail: `linked map area "${linkedMapArea}" is already referenced by active spec ${file}` });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// 3. Build report
|
|
485
|
+
const reportLines = [`spec-promote: ${(0, node_path_1.basename)(source)}`];
|
|
486
|
+
if (conflicts.length === 0) {
|
|
487
|
+
reportLines.push(" no content conflicts detected");
|
|
488
|
+
reportLines.push(" no map area conflicts detected");
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
reportLines.push(` ${conflicts.length} conflict(s) detected:`);
|
|
492
|
+
for (const c of conflicts) {
|
|
493
|
+
reportLines.push(` [${c.type}] ${c.detail}`);
|
|
494
|
+
}
|
|
495
|
+
if (!options.approve) {
|
|
496
|
+
reportLines.push("");
|
|
497
|
+
reportLines.push(" Halted. Resolve conflicts or rerun with --approve to override.");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const report = reportLines.join("\n");
|
|
501
|
+
// 4. Halt if conflicts and not approved
|
|
502
|
+
if (conflicts.length > 0 && !options.approve) {
|
|
503
|
+
appendLifecycle(lifecyclePath, {
|
|
504
|
+
event: "spec-promote-halted",
|
|
505
|
+
run_id: runId,
|
|
506
|
+
source,
|
|
507
|
+
conflicts,
|
|
508
|
+
timestamp: new Date().toISOString(),
|
|
509
|
+
});
|
|
510
|
+
return { source, destination: "", runId, lifecyclePath, conflicts, halted: true, report };
|
|
511
|
+
}
|
|
512
|
+
// 5. Promote
|
|
513
|
+
const activeDir = (0, node_path_1.join)(repoRoot, "smartdocs", "specs", "active");
|
|
514
|
+
(0, node_fs_1.mkdirSync)(activeDir, { recursive: true });
|
|
515
|
+
const destination = (0, node_path_1.join)(activeDir, (0, node_path_1.basename)(source));
|
|
516
|
+
if ((0, node_fs_1.existsSync)(destination)) {
|
|
517
|
+
throw new Error(`Destination already exists: ${destination}`);
|
|
518
|
+
}
|
|
519
|
+
(0, node_fs_1.renameSync)(source, destination);
|
|
520
|
+
if ((0, node_fs_1.existsSync)(provenanceSrcPath)) {
|
|
521
|
+
(0, node_fs_1.renameSync)(provenanceSrcPath, destination.replace(/\.md$/, ".provenance.json"));
|
|
522
|
+
}
|
|
523
|
+
appendLifecycle(lifecyclePath, {
|
|
524
|
+
event: "spec-promote",
|
|
525
|
+
run_id: runId,
|
|
526
|
+
source,
|
|
527
|
+
destination,
|
|
528
|
+
conflicts_at_promote: conflicts.length,
|
|
529
|
+
approved: options.approve ?? false,
|
|
530
|
+
timestamp: new Date().toISOString(),
|
|
531
|
+
});
|
|
532
|
+
return { source, destination, runId, lifecyclePath, conflicts, halted: false, report };
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Migrate .provenance.json sidecars to in-file frontmatter stamps.
|
|
536
|
+
*
|
|
537
|
+
* Walks all of smartdocs/ recursively, finds every .provenance.json file,
|
|
538
|
+
* reads the paired .md file, calls stampIngestFrontMatter() to embed provenance
|
|
539
|
+
* in its frontmatter, writes the updated .md, then deletes the sidecar.
|
|
540
|
+
*
|
|
541
|
+
* In dry-run mode: reports what would be done without writing or deleting anything.
|
|
542
|
+
*/
|
|
543
|
+
function migrateProvenance(options) {
|
|
544
|
+
const repoRoot = (0, node_path_1.resolve)(options.repoRoot);
|
|
545
|
+
const runId = options.runId ?? `polaris-provenance-migration-${new Date().toISOString().slice(0, 10)}-001`;
|
|
546
|
+
const lifecyclePath = (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-doctrine", runId, "lifecycle.jsonl");
|
|
547
|
+
const smartdocsDir = (0, node_path_1.join)(repoRoot, "smartdocs");
|
|
548
|
+
if (!(0, node_fs_1.existsSync)(smartdocsDir)) {
|
|
549
|
+
throw new Error(`migrateProvenance: smartdocs/ directory not found at ${smartdocsDir}`);
|
|
550
|
+
}
|
|
551
|
+
const records = [];
|
|
552
|
+
function walk(dir) {
|
|
553
|
+
let entries;
|
|
554
|
+
try {
|
|
555
|
+
entries = (0, node_fs_1.readdirSync)(dir);
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
for (const entry of entries) {
|
|
561
|
+
const full = (0, node_path_1.join)(dir, entry);
|
|
562
|
+
try {
|
|
563
|
+
const stat = (0, node_fs_1.statSync)(full);
|
|
564
|
+
if (stat.isDirectory()) {
|
|
565
|
+
walk(full);
|
|
566
|
+
}
|
|
567
|
+
else if (entry.endsWith(".provenance.json")) {
|
|
568
|
+
const mdPath = full.replace(/\.provenance\.json$/, ".md");
|
|
569
|
+
const relSidecar = (0, node_path_1.relative)(repoRoot, full).replace(/\\/g, "/");
|
|
570
|
+
const relMd = (0, node_path_1.relative)(repoRoot, mdPath).replace(/\\/g, "/");
|
|
571
|
+
if (!(0, node_fs_1.existsSync)(mdPath)) {
|
|
572
|
+
records.push({ sidecar: relSidecar, mdFile: relMd, status: "skipped-no-md" });
|
|
573
|
+
appendLifecycle(lifecyclePath, {
|
|
574
|
+
event: "provenance-migration-skipped",
|
|
575
|
+
run_id: runId,
|
|
576
|
+
sidecar: relSidecar,
|
|
577
|
+
reason: "no paired .md file",
|
|
578
|
+
timestamp: new Date().toISOString(),
|
|
579
|
+
});
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (options.dryRun) {
|
|
583
|
+
records.push({ sidecar: relSidecar, mdFile: relMd, status: "skipped-dry-run" });
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
// Read sidecar fields to build the stamp
|
|
588
|
+
const sidecar = JSON.parse((0, node_fs_1.readFileSync)(full, "utf-8"));
|
|
589
|
+
const mdContent = (0, node_fs_1.readFileSync)(mdPath, "utf-8");
|
|
590
|
+
const stamp = {
|
|
591
|
+
originalPath: sidecar.originalPath ?? relMd,
|
|
592
|
+
classifiedAs: sidecar.classifiedAs ?? "unknown",
|
|
593
|
+
ingestRunId: sidecar.ingestRunId ?? "migrated",
|
|
594
|
+
ingestClusterId: sidecar.ingestClusterId ?? null,
|
|
595
|
+
linkedMapArea: sidecar.linkedMapArea ?? null,
|
|
596
|
+
ingestedAt: sidecar.ingestedAt ?? new Date().toISOString(),
|
|
597
|
+
};
|
|
598
|
+
const stamped = stampIngestFrontMatter(mdContent, stamp);
|
|
599
|
+
(0, node_fs_1.writeFileSync)(mdPath, stamped, "utf-8");
|
|
600
|
+
(0, node_fs_1.unlinkSync)(full);
|
|
601
|
+
records.push({ sidecar: relSidecar, mdFile: relMd, status: "stamped" });
|
|
602
|
+
appendLifecycle(lifecyclePath, {
|
|
603
|
+
event: "provenance-migration-stamped",
|
|
604
|
+
run_id: runId,
|
|
605
|
+
sidecar: relSidecar,
|
|
606
|
+
md_file: relMd,
|
|
607
|
+
timestamp: new Date().toISOString(),
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
612
|
+
records.push({ sidecar: relSidecar, mdFile: relMd, status: "error", error: msg });
|
|
613
|
+
appendLifecycle(lifecyclePath, {
|
|
614
|
+
event: "provenance-migration-error",
|
|
615
|
+
run_id: runId,
|
|
616
|
+
sidecar: relSidecar,
|
|
617
|
+
error: msg,
|
|
618
|
+
timestamp: new Date().toISOString(),
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
walk(smartdocsDir);
|
|
629
|
+
const stamped = records.filter((r) => r.status === "stamped").length;
|
|
630
|
+
const skipped = records.filter((r) => r.status === "skipped-no-md" || r.status === "skipped-dry-run").length;
|
|
631
|
+
const errors = records.filter((r) => r.status === "error").length;
|
|
632
|
+
return { runId, lifecyclePath: (0, node_path_1.relative)(repoRoot, lifecyclePath).replace(/\\/g, "/"), records, stamped, skipped, errors };
|
|
633
|
+
}
|