@lsctech/polaris 0.1.3 → 0.2.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/dist/governance/authority-risk.js +56 -0
- package/dist/governance/index.js +12 -0
- package/dist/governance/review-packet.js +114 -0
- package/dist/governance/routing.js +63 -0
- package/dist/governance/types.js +8 -0
- package/dist/smartdocs-engine/index.js +16 -2
- package/dist/smartdocs-engine/ingest.js +146 -13
- package/package.json +1 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.computeAuthorityRisk = computeAuthorityRisk;
|
|
4
|
+
const HIGH_AUTHORITY_PATH_SEGMENTS = [
|
|
5
|
+
"doctrine/active",
|
|
6
|
+
"architecture",
|
|
7
|
+
"decisions",
|
|
8
|
+
"specs/active",
|
|
9
|
+
];
|
|
10
|
+
const MEDIUM_AUTHORITY_PATH_SEGMENTS = [
|
|
11
|
+
"doctrine/candidate",
|
|
12
|
+
];
|
|
13
|
+
const HIGH_AUTHORITY_CLASSIFICATIONS = new Set([
|
|
14
|
+
"architecture",
|
|
15
|
+
"decision",
|
|
16
|
+
"spec-active",
|
|
17
|
+
]);
|
|
18
|
+
const MEDIUM_AUTHORITY_CLASSIFICATIONS = new Set([
|
|
19
|
+
"doctrine-candidate",
|
|
20
|
+
]);
|
|
21
|
+
function containsPathSegment(normalized, segment) {
|
|
22
|
+
return (normalized === segment ||
|
|
23
|
+
normalized.startsWith(segment + "/") ||
|
|
24
|
+
normalized.endsWith("/" + segment) ||
|
|
25
|
+
normalized.includes("/" + segment + "/"));
|
|
26
|
+
}
|
|
27
|
+
function riskFromPath(destinationPath) {
|
|
28
|
+
const normalized = destinationPath.replace(/\\/g, "/");
|
|
29
|
+
for (const seg of HIGH_AUTHORITY_PATH_SEGMENTS) {
|
|
30
|
+
if (containsPathSegment(normalized, seg))
|
|
31
|
+
return "high";
|
|
32
|
+
}
|
|
33
|
+
for (const seg of MEDIUM_AUTHORITY_PATH_SEGMENTS) {
|
|
34
|
+
if (containsPathSegment(normalized, seg))
|
|
35
|
+
return "medium";
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function riskFromClassification(classification) {
|
|
40
|
+
if (HIGH_AUTHORITY_CLASSIFICATIONS.has(classification))
|
|
41
|
+
return "high";
|
|
42
|
+
if (MEDIUM_AUTHORITY_CLASSIFICATIONS.has(classification))
|
|
43
|
+
return "medium";
|
|
44
|
+
return "low";
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Determine the authority risk of placing a document at the given destination.
|
|
48
|
+
* Path wins over classification when they disagree, because authority is
|
|
49
|
+
* determined by where an artifact lands, not what it was classified as.
|
|
50
|
+
*/
|
|
51
|
+
function computeAuthorityRisk(classification, destinationPath) {
|
|
52
|
+
const pathRisk = riskFromPath(destinationPath);
|
|
53
|
+
if (pathRisk !== null)
|
|
54
|
+
return pathRisk;
|
|
55
|
+
return riskFromClassification(classification);
|
|
56
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyReviewDecisions = exports.readReviewQueue = exports.writeReviewQueue = exports.buildReviewPacket = exports.route = exports.computeAuthorityRisk = void 0;
|
|
4
|
+
var authority_risk_js_1 = require("./authority-risk.js");
|
|
5
|
+
Object.defineProperty(exports, "computeAuthorityRisk", { enumerable: true, get: function () { return authority_risk_js_1.computeAuthorityRisk; } });
|
|
6
|
+
var routing_js_1 = require("./routing.js");
|
|
7
|
+
Object.defineProperty(exports, "route", { enumerable: true, get: function () { return routing_js_1.route; } });
|
|
8
|
+
var review_packet_js_1 = require("./review-packet.js");
|
|
9
|
+
Object.defineProperty(exports, "buildReviewPacket", { enumerable: true, get: function () { return review_packet_js_1.buildReviewPacket; } });
|
|
10
|
+
Object.defineProperty(exports, "writeReviewQueue", { enumerable: true, get: function () { return review_packet_js_1.writeReviewQueue; } });
|
|
11
|
+
Object.defineProperty(exports, "readReviewQueue", { enumerable: true, get: function () { return review_packet_js_1.readReviewQueue; } });
|
|
12
|
+
Object.defineProperty(exports, "applyReviewDecisions", { enumerable: true, get: function () { return review_packet_js_1.applyReviewDecisions; } });
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildReviewPacket = buildReviewPacket;
|
|
4
|
+
exports.writeReviewQueue = writeReviewQueue;
|
|
5
|
+
exports.readReviewQueue = readReviewQueue;
|
|
6
|
+
exports.applyReviewDecisions = applyReviewDecisions;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
9
|
+
function buildReviewPacket(result, sourcePath, proposedDestination, conflicts, outcomeReason, recommendation) {
|
|
10
|
+
return {
|
|
11
|
+
sourcePath,
|
|
12
|
+
proposedDestination,
|
|
13
|
+
classificationConfidence: result.classificationConfidence,
|
|
14
|
+
destinationCertainty: result.destinationCertainty,
|
|
15
|
+
authorityRisk: result.authorityRisk,
|
|
16
|
+
reasoning: result.reasoning,
|
|
17
|
+
conflicts,
|
|
18
|
+
recommendation,
|
|
19
|
+
outcomeReason,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const RISK_ORDER = { high: 0, medium: 1, low: 2 };
|
|
23
|
+
function renderPacketMarkdown(p) {
|
|
24
|
+
const lines = [
|
|
25
|
+
`## ${p.reviewDecision ? `✓ ${p.reviewDecision}` : "review-required"} · ${p.authorityRisk.toUpperCase()} authority risk`,
|
|
26
|
+
``,
|
|
27
|
+
`**Source:** ${p.sourcePath}`,
|
|
28
|
+
`**Proposed destination:** ${p.proposedDestination}`,
|
|
29
|
+
`**Classification confidence:** ${p.classificationConfidence.toFixed(2)}`,
|
|
30
|
+
`**Destination certainty:** ${p.destinationCertainty.toFixed(2)}`,
|
|
31
|
+
`**Outcome reason:** ${p.outcomeReason}`,
|
|
32
|
+
``,
|
|
33
|
+
`**Reasoning:**`,
|
|
34
|
+
...p.reasoning.map((r) => `- ${r}`),
|
|
35
|
+
``,
|
|
36
|
+
`**Conflicts:** ${p.conflicts.length === 0 ? "none detected" : p.conflicts.join(", ")}`,
|
|
37
|
+
``,
|
|
38
|
+
`**Recommendation:** ${p.recommendation}`,
|
|
39
|
+
`**Review decision:** ${p.reviewDecision ?? "← set this to \`approve\`, \`reject\`, or \`defer\`"}`,
|
|
40
|
+
];
|
|
41
|
+
return lines.join("\n");
|
|
42
|
+
}
|
|
43
|
+
function groupAndSort(packets) {
|
|
44
|
+
return [...packets].sort((a, b) => {
|
|
45
|
+
const riskCompare = RISK_ORDER[a.authorityRisk] - RISK_ORDER[b.authorityRisk];
|
|
46
|
+
if (riskCompare !== 0)
|
|
47
|
+
return riskCompare;
|
|
48
|
+
return a.sourcePath.localeCompare(b.sourcePath);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Write _review-queue.json (canonical) and _review-queue.md (display-only) to outputDir.
|
|
53
|
+
* Markdown is regenerated from JSON — never parse markdown to recover decisions.
|
|
54
|
+
*/
|
|
55
|
+
function writeReviewQueue(packets, runId, outputDir) {
|
|
56
|
+
(0, node_fs_1.mkdirSync)(outputDir, { recursive: true });
|
|
57
|
+
const queueFile = {
|
|
58
|
+
generated_at: new Date().toISOString(),
|
|
59
|
+
run_id: runId,
|
|
60
|
+
packets,
|
|
61
|
+
};
|
|
62
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(outputDir, "_review-queue.json"), JSON.stringify(queueFile, null, 2) + "\n", "utf-8");
|
|
63
|
+
const sorted = groupAndSort(packets);
|
|
64
|
+
const sections = sorted.map(renderPacketMarkdown).join("\n\n---\n\n");
|
|
65
|
+
const md = [
|
|
66
|
+
`# Polaris Review Queue`,
|
|
67
|
+
``,
|
|
68
|
+
`**Run ID:** ${runId}`,
|
|
69
|
+
`**Generated:** ${queueFile.generated_at}`,
|
|
70
|
+
`**Pending review:** ${packets.length} document(s)`,
|
|
71
|
+
``,
|
|
72
|
+
`> Markdown is display-only. Edit \`_review-queue.json\` to set \`reviewDecision\` fields.`,
|
|
73
|
+
`> Rerun \`polaris docs ingest\` to apply decisions.`,
|
|
74
|
+
``,
|
|
75
|
+
`---`,
|
|
76
|
+
``,
|
|
77
|
+
sections,
|
|
78
|
+
].join("\n");
|
|
79
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(outputDir, "_review-queue.md"), md, "utf-8");
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Read review queue from JSON. Returns empty array if no queue file exists.
|
|
83
|
+
* Never reads markdown.
|
|
84
|
+
*/
|
|
85
|
+
function readReviewQueue(outputDir) {
|
|
86
|
+
const jsonPath = (0, node_path_1.join)(outputDir, "_review-queue.json");
|
|
87
|
+
if (!(0, node_fs_1.existsSync)(jsonPath))
|
|
88
|
+
return [];
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(jsonPath, "utf-8"));
|
|
91
|
+
return Array.isArray(parsed.packets) ? parsed.packets : [];
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Merge user decisions from a reviewed queue into a set of pending packets.
|
|
99
|
+
* Matches by sourcePath. Unmatched pending packets are returned unchanged.
|
|
100
|
+
*/
|
|
101
|
+
function applyReviewDecisions(pending, reviewed) {
|
|
102
|
+
const bySource = new Map(reviewed.map((r) => [r.sourcePath, r]));
|
|
103
|
+
return pending.map((p) => {
|
|
104
|
+
const decision = bySource.get(p.sourcePath);
|
|
105
|
+
if (!decision?.reviewDecision)
|
|
106
|
+
return p;
|
|
107
|
+
return {
|
|
108
|
+
...p,
|
|
109
|
+
reviewDecision: decision.reviewDecision,
|
|
110
|
+
reviewedAt: decision.reviewedAt,
|
|
111
|
+
reviewedBy: decision.reviewedBy,
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.route = route;
|
|
4
|
+
function buildMinimalPacket(result, outcomeReason, recommendation) {
|
|
5
|
+
return {
|
|
6
|
+
sourcePath: "",
|
|
7
|
+
proposedDestination: "",
|
|
8
|
+
classificationConfidence: result.classificationConfidence,
|
|
9
|
+
destinationCertainty: result.destinationCertainty,
|
|
10
|
+
authorityRisk: result.authorityRisk,
|
|
11
|
+
reasoning: result.reasoning,
|
|
12
|
+
conflicts: [],
|
|
13
|
+
recommendation,
|
|
14
|
+
outcomeReason,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Pure routing function — no I/O.
|
|
19
|
+
* Implements the five-row governance decision table:
|
|
20
|
+
*
|
|
21
|
+
* Row 1: high conf + high dest + low risk → auto-route
|
|
22
|
+
* Row 2: high conf + high dest + med risk → candidate
|
|
23
|
+
* Row 3: high conf + low dest + med risk → review-required
|
|
24
|
+
* Row 4: high conf + any dest + high risk → review-required
|
|
25
|
+
* Row 5: low conf + any → review-required
|
|
26
|
+
*
|
|
27
|
+
* sourcePath and proposedDestination in the returned packet are empty strings;
|
|
28
|
+
* the caller fills them in after routing.
|
|
29
|
+
*/
|
|
30
|
+
function route(result, thresholds) {
|
|
31
|
+
const highConf = result.classificationConfidence >= thresholds.confidence;
|
|
32
|
+
const highDest = result.destinationCertainty >= thresholds.destinationCertainty;
|
|
33
|
+
// Row 1
|
|
34
|
+
if (highConf && highDest && result.authorityRisk === "low") {
|
|
35
|
+
return { outcome: "auto-route" };
|
|
36
|
+
}
|
|
37
|
+
// Row 2
|
|
38
|
+
if (highConf && highDest && result.authorityRisk === "medium") {
|
|
39
|
+
return {
|
|
40
|
+
outcome: "candidate",
|
|
41
|
+
reviewPacket: buildMinimalPacket(result, "Routed to candidate: classification and destination certainty are high, but canonical approval is still required.", "approve"),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// Row 3
|
|
45
|
+
if (highConf && !highDest && result.authorityRisk === "medium") {
|
|
46
|
+
return {
|
|
47
|
+
outcome: "review-required",
|
|
48
|
+
reviewPacket: buildMinimalPacket(result, "Routed to review-required: destination certainty is below threshold for medium authority risk placement.", "defer"),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// Row 4
|
|
52
|
+
if (highConf && result.authorityRisk === "high") {
|
|
53
|
+
return {
|
|
54
|
+
outcome: "review-required",
|
|
55
|
+
reviewPacket: buildMinimalPacket(result, "Routed to review-required: high authority risk destination requires user approval.", "defer"),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// Row 5
|
|
59
|
+
return {
|
|
60
|
+
outcome: "review-required",
|
|
61
|
+
reviewPacket: buildMinimalPacket(result, `Routed to review-required: classification confidence ${result.classificationConfidence.toFixed(2)} is below threshold ${thresholds.confidence}.`, "defer"),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared types for document governance decisions: authority risk assessment, routing,
|
|
4
|
+
* and review packets. Intentionally decoupled from Smart Docs vocabulary so this module
|
|
5
|
+
* can be reused by any Polaris workflow that requires authority-boundary enforcement.
|
|
6
|
+
* The `classification` field is opaque (string) — callers define their own vocabulary.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -45,14 +45,25 @@ function createDocsCommand(options = {}) {
|
|
|
45
45
|
docs
|
|
46
46
|
.command("ingest [path]")
|
|
47
47
|
.description("Classify and place docs into the smartdocs/ canonical authority structure")
|
|
48
|
-
.option("--file <path>", "Single file to ingest")
|
|
49
48
|
.option("--batch <cluster-id>", "Cluster ID for bounded batch ingest (reads .polaris/docs-ingest/<cluster-id>.json)")
|
|
50
49
|
.option("--cluster <id>", "Alias for --batch")
|
|
51
50
|
.option("--files <paths...>", "Bounded batch file list")
|
|
52
51
|
.option("--dry-run", "Classify and report without moving files")
|
|
53
52
|
.option("--approve-authority", "Allow placement in high-authority docs areas")
|
|
53
|
+
.option("--file <path>", "Single file to ingest; also scopes --approve-authority to a single file path")
|
|
54
|
+
.option("--from-review-queue", "scope --approve-authority to items in the review queue")
|
|
55
|
+
.option("--decision-id <id>", "scope --approve-authority to a specific decision ID")
|
|
56
|
+
.option("--interactive", "pause and prompt for review decisions on each review-required document")
|
|
57
|
+
.option("--confidence-threshold <n>", "classification confidence threshold (0–1, default 0.75)", parseFloat)
|
|
58
|
+
.option("--destination-certainty-threshold <n>", "destination certainty threshold (0–1, default 0.70)", parseFloat)
|
|
54
59
|
.option("-r, --repo-root <path>", "Repository root", defaultRepoRoot)
|
|
55
|
-
.action((pathArg,
|
|
60
|
+
.action(async (pathArg, opts) => {
|
|
61
|
+
if (opts.approveAuthority && !opts.file && !opts.fromReviewQueue && !opts.decisionId) {
|
|
62
|
+
console.error("error: --approve-authority requires an explicit scope: --file <path>, --from-review-queue, or --decision-id <id>");
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
// Alias opts to options for compatibility with existing code below
|
|
66
|
+
const options = opts;
|
|
56
67
|
const clusterId = options.batch ?? options.cluster;
|
|
57
68
|
// Validate clusterId
|
|
58
69
|
if (clusterId && !/^[A-Za-z0-9_-]+$/.test(clusterId)) {
|
|
@@ -111,6 +122,9 @@ function createDocsCommand(options = {}) {
|
|
|
111
122
|
dryRun: options.dryRun,
|
|
112
123
|
clusterId,
|
|
113
124
|
approveAuthority: options.approveAuthority,
|
|
125
|
+
interactive: opts.interactive,
|
|
126
|
+
confidenceThreshold: opts.confidenceThreshold,
|
|
127
|
+
destinationCertaintyThreshold: opts.destinationCertaintyThreshold,
|
|
114
128
|
});
|
|
115
129
|
(0, ingest_js_1.printIngestResults)(results);
|
|
116
130
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SMART_DOCS_SCAFFOLD_DIRS = exports.CANONICAL_TARGET = void 0;
|
|
4
4
|
exports.classifyDoc = classifyDoc;
|
|
5
|
+
exports.classifyDocWithConfidence = classifyDocWithConfidence;
|
|
5
6
|
exports.ensureDocsScaffold = ensureDocsScaffold;
|
|
6
7
|
exports.ingestDocs = ingestDocs;
|
|
7
8
|
exports.printIngestResults = printIngestResults;
|
|
@@ -13,6 +14,8 @@ const monotonic_timestamp_js_1 = require("../utils/monotonic-timestamp.js");
|
|
|
13
14
|
const smartdoc_ignore_js_1 = require("./smartdoc-ignore.js");
|
|
14
15
|
const doctrine_js_1 = require("./doctrine.js");
|
|
15
16
|
const summary_delta_js_1 = require("../cognition/summary-delta.js");
|
|
17
|
+
const authority_risk_js_1 = require("../governance/authority-risk.js");
|
|
18
|
+
const index_js_1 = require("../governance/index.js");
|
|
16
19
|
exports.CANONICAL_TARGET = "smartdocs";
|
|
17
20
|
const DOCS_INGEST_STATE_FILE = ".taskchain_artifacts/polaris-docs-ingest/current-state.json";
|
|
18
21
|
const DOCS_INGEST_RUNS_DIR = ".taskchain_artifacts/polaris-docs-ingest/runs";
|
|
@@ -23,7 +26,7 @@ const TARGET_DIRS = {
|
|
|
23
26
|
"spec-raw": `${exports.CANONICAL_TARGET}/raw`,
|
|
24
27
|
"spec-active": `${exports.CANONICAL_TARGET}/specs/active`,
|
|
25
28
|
"audit-finding": `${exports.CANONICAL_TARGET}/audits/findings`,
|
|
26
|
-
"doctrine-candidate": `${exports.CANONICAL_TARGET}/doctrine/
|
|
29
|
+
"doctrine-candidate": `${exports.CANONICAL_TARGET}/doctrine/candidate`,
|
|
27
30
|
architecture: `${exports.CANONICAL_TARGET}/architecture`,
|
|
28
31
|
decision: `${exports.CANONICAL_TARGET}/decisions`,
|
|
29
32
|
"deprecated-noise": `${exports.CANONICAL_TARGET}/runtime/generated`,
|
|
@@ -38,7 +41,6 @@ exports.SMART_DOCS_SCAFFOLD_DIRS = [
|
|
|
38
41
|
`${exports.CANONICAL_TARGET}/doctrine/active`,
|
|
39
42
|
`${exports.CANONICAL_TARGET}/doctrine/deprecated`,
|
|
40
43
|
];
|
|
41
|
-
const APPROVAL_REQUIRED = new Set(["spec-active", "architecture", "decision"]);
|
|
42
44
|
/**
|
|
43
45
|
* Conflict detection uses subject-verb-keyword triples rather than bare
|
|
44
46
|
* verb-keyword pairs. A conflict only fires when both the subject AND the
|
|
@@ -107,6 +109,87 @@ function classifyDoc(content, filePath = "") {
|
|
|
107
109
|
}
|
|
108
110
|
return "spec-raw";
|
|
109
111
|
}
|
|
112
|
+
const DOMAIN_KEYWORDS_RE = /\b(loop|map|finalize|config|cli|docs|doctrine|spec|audit)\b/i;
|
|
113
|
+
function classifyDocWithConfidence(content, filePath) {
|
|
114
|
+
const classification = classifyDoc(content, filePath ?? "");
|
|
115
|
+
const reasoning = [];
|
|
116
|
+
// --- classificationConfidence ---
|
|
117
|
+
let classConf = 0.3;
|
|
118
|
+
const docType = frontMatterValue(content, "doc-type");
|
|
119
|
+
if (docType) {
|
|
120
|
+
classConf += 0.4;
|
|
121
|
+
reasoning.push(`Explicit frontmatter doc-type: ${docType}`);
|
|
122
|
+
}
|
|
123
|
+
const authority = frontMatterValue(content, "authority")?.toLowerCase();
|
|
124
|
+
const status = frontMatterValue(content, "status")?.toLowerCase();
|
|
125
|
+
const hasSupportingFm = authority || status;
|
|
126
|
+
if (hasSupportingFm) {
|
|
127
|
+
// check alignment: authority or status loosely matches classification
|
|
128
|
+
const classLower = classification.toLowerCase();
|
|
129
|
+
if ((authority && classLower.includes(authority)) ||
|
|
130
|
+
(status && classLower.includes(status))) {
|
|
131
|
+
classConf += 0.2;
|
|
132
|
+
reasoning.push(`Frontmatter authority/status aligns with classification`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Count independent keyword signals from classifyDoc logic
|
|
136
|
+
const lower = `${filePath ?? ""}\n${content}`.toLowerCase();
|
|
137
|
+
let keywordSignals = 0;
|
|
138
|
+
if (lower.includes("run report") || lower.includes("run-report"))
|
|
139
|
+
keywordSignals++;
|
|
140
|
+
if (lower.includes("runtime summary") || lower.includes("session summary"))
|
|
141
|
+
keywordSignals++;
|
|
142
|
+
if (lower.includes("audit finding") || lower.includes("vulnerability") || lower.includes("security audit"))
|
|
143
|
+
keywordSignals++;
|
|
144
|
+
if (lower.includes("doctrine") || lower.includes("must always") || lower.includes("never silently"))
|
|
145
|
+
keywordSignals++;
|
|
146
|
+
if (lower.includes("architecture decision record") || /^#\s*adr[:\s-]/im.test(content))
|
|
147
|
+
keywordSignals++;
|
|
148
|
+
if (lower.includes("architecture") || lower.includes("structural design"))
|
|
149
|
+
keywordSignals++;
|
|
150
|
+
if (lower.includes("active spec"))
|
|
151
|
+
keywordSignals++;
|
|
152
|
+
if (lower.includes("acceptance criteria") || lower.includes("implementation plan"))
|
|
153
|
+
keywordSignals++;
|
|
154
|
+
if (keywordSignals >= 2) {
|
|
155
|
+
classConf += 0.3;
|
|
156
|
+
reasoning.push(`Multiple (${keywordSignals}) independent keyword signals matched`);
|
|
157
|
+
}
|
|
158
|
+
else if (keywordSignals === 1) {
|
|
159
|
+
classConf += 0.1;
|
|
160
|
+
reasoning.push(`Single keyword signal matched`);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
reasoning.push(`Default spec-raw fallback with no signals`);
|
|
164
|
+
}
|
|
165
|
+
classConf = Math.min(classConf, 1.0);
|
|
166
|
+
// --- destinationCertainty ---
|
|
167
|
+
let destCert = 0.2;
|
|
168
|
+
const linkedMapArea = frontMatterValue(content, "linked-map-area");
|
|
169
|
+
if (linkedMapArea) {
|
|
170
|
+
destCert += 0.4;
|
|
171
|
+
reasoning.push(`Frontmatter linked-map-area: ${linkedMapArea}`);
|
|
172
|
+
}
|
|
173
|
+
if (filePath && DOMAIN_KEYWORDS_RE.test(filePath)) {
|
|
174
|
+
destCert += 0.2;
|
|
175
|
+
reasoning.push(`Domain keyword found in filename`);
|
|
176
|
+
}
|
|
177
|
+
const isDefaultFallback = classification === "spec-raw" && keywordSignals === 0 && !docType;
|
|
178
|
+
if (!isDefaultFallback) {
|
|
179
|
+
destCert += 0.2;
|
|
180
|
+
reasoning.push(`Classification is not default fallback`);
|
|
181
|
+
}
|
|
182
|
+
destCert = Math.min(destCert, 1.0);
|
|
183
|
+
const proposedDest = TARGET_DIRS[classification] ?? "";
|
|
184
|
+
const authorityRisk = (0, authority_risk_js_1.computeAuthorityRisk)(classification, proposedDest);
|
|
185
|
+
return {
|
|
186
|
+
classification,
|
|
187
|
+
classificationConfidence: classConf,
|
|
188
|
+
destinationCertainty: destCert,
|
|
189
|
+
authorityRisk,
|
|
190
|
+
reasoning,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
110
193
|
function readCurrentState(repoRoot) {
|
|
111
194
|
return readJson((0, node_path_1.resolve)(repoRoot, DOCS_INGEST_STATE_FILE), {});
|
|
112
195
|
}
|
|
@@ -315,6 +398,9 @@ function ingestDocs(files, options) {
|
|
|
315
398
|
const priorState = readCurrentState(repoRoot);
|
|
316
399
|
const clusterId = options.clusterId ?? null;
|
|
317
400
|
const runId = generateRunId(repoRoot);
|
|
401
|
+
const rawDir = (0, node_path_1.resolve)(repoRoot, exports.CANONICAL_TARGET, "raw");
|
|
402
|
+
// Read existing review queue to apply prior human decisions
|
|
403
|
+
const priorQueue = (0, index_js_1.readReviewQueue)(rawDir);
|
|
318
404
|
// Emit run-start telemetry (STOP CONDITION if this write fails)
|
|
319
405
|
emitRunStartTelemetry(repoRoot, runId, priorState.run_id ?? null);
|
|
320
406
|
const telPath = docsIngestTelemetryPath(repoRoot, runId);
|
|
@@ -349,10 +435,6 @@ function ingestDocs(files, options) {
|
|
|
349
435
|
if (!(0, node_fs_1.existsSync)(absSource))
|
|
350
436
|
throw new Error(`polaris docs ingest: file not found: ${source}`);
|
|
351
437
|
const content = (0, node_fs_1.readFileSync)(absSource, "utf-8");
|
|
352
|
-
const classification = classifyDoc(content, relSource);
|
|
353
|
-
if (APPROVAL_REQUIRED.has(classification) && !options.approveAuthority) {
|
|
354
|
-
throw new Error(`polaris docs ingest: ${classification} requires explicit approval; rerun with --approve-authority`);
|
|
355
|
-
}
|
|
356
438
|
// Conflict detection against active doctrine (STOP CONDITION)
|
|
357
439
|
const conflict = detectDoctrineConflict(content, repoRoot);
|
|
358
440
|
if (conflict) {
|
|
@@ -365,8 +447,46 @@ function ingestDocs(files, options) {
|
|
|
365
447
|
});
|
|
366
448
|
throw new Error(`polaris docs ingest: conflict detected — ${conflict.detail}`);
|
|
367
449
|
}
|
|
450
|
+
// ── Governance routing ────────────────────────────────────────────────
|
|
451
|
+
const classificationResult = classifyDocWithConfidence(content, relSource);
|
|
452
|
+
const docClassification = classificationResult.classification;
|
|
453
|
+
const thresholds = {
|
|
454
|
+
confidence: options.confidenceThreshold ?? 0.75,
|
|
455
|
+
destinationCertainty: options.destinationCertaintyThreshold ?? 0.70,
|
|
456
|
+
};
|
|
457
|
+
const routingDecision = (0, index_js_1.route)(classificationResult, thresholds);
|
|
458
|
+
const proposedDest = (0, node_path_1.relative)(repoRoot, (0, node_path_1.join)((0, node_path_1.resolve)(repoRoot, TARGET_DIRS[docClassification]), (0, node_path_1.basename)(absSource))).replace(/\\/g, "/");
|
|
459
|
+
// review-required: leave in raw/, emit packet, skip move
|
|
460
|
+
if (routingDecision.outcome === "review-required") {
|
|
461
|
+
const packet = {
|
|
462
|
+
...routingDecision.reviewPacket,
|
|
463
|
+
sourcePath: relSource,
|
|
464
|
+
proposedDestination: proposedDest,
|
|
465
|
+
conflicts: [],
|
|
466
|
+
};
|
|
467
|
+
emitTelemetry(telPath, runId, {
|
|
468
|
+
event: "docs-ingest-review-required",
|
|
469
|
+
file: relSource,
|
|
470
|
+
classification: docClassification,
|
|
471
|
+
outcome_reason: packet.outcomeReason,
|
|
472
|
+
cluster_id: clusterId,
|
|
473
|
+
});
|
|
474
|
+
results.push({
|
|
475
|
+
sourcePath: relSource,
|
|
476
|
+
destinationPath: relSource,
|
|
477
|
+
classification: docClassification,
|
|
478
|
+
linkedMapArea: null,
|
|
479
|
+
runId,
|
|
480
|
+
dryRun: Boolean(options.dryRun),
|
|
481
|
+
nearestSummary: null,
|
|
482
|
+
summaryDeltaWarranted: false,
|
|
483
|
+
routingDecision: "review-required",
|
|
484
|
+
reviewPacket: packet,
|
|
485
|
+
});
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
368
488
|
const { label: linkedMapArea, entry: linkedEntry } = deriveLinkedArea(content, routes);
|
|
369
|
-
const targetDir = (0, node_path_1.resolve)(repoRoot, TARGET_DIRS[
|
|
489
|
+
const targetDir = (0, node_path_1.resolve)(repoRoot, TARGET_DIRS[docClassification]);
|
|
370
490
|
(0, node_fs_1.mkdirSync)(targetDir, { recursive: true });
|
|
371
491
|
const rawDestination = (0, node_path_1.join)(targetDir, (0, node_path_1.basename)(absSource));
|
|
372
492
|
const destination = (0, node_path_1.resolve)(rawDestination) === (0, node_path_1.resolve)(absSource)
|
|
@@ -379,7 +499,7 @@ function ingestDocs(files, options) {
|
|
|
379
499
|
emitTelemetry(telPath, runId, {
|
|
380
500
|
event: "docs-ingest-classified",
|
|
381
501
|
file: relSource,
|
|
382
|
-
classification,
|
|
502
|
+
classification: docClassification,
|
|
383
503
|
destination: relDestination,
|
|
384
504
|
linked_map_area: linkedMapArea,
|
|
385
505
|
cluster_id: clusterId,
|
|
@@ -390,7 +510,7 @@ function ingestDocs(files, options) {
|
|
|
390
510
|
}
|
|
391
511
|
const stampedContent = (0, doctrine_js_1.stampIngestFrontMatter)((0, node_fs_1.readFileSync)(destination, "utf-8"), {
|
|
392
512
|
originalPath: relSource,
|
|
393
|
-
classifiedAs:
|
|
513
|
+
classifiedAs: docClassification,
|
|
394
514
|
ingestRunId: runId,
|
|
395
515
|
ingestClusterId: clusterId,
|
|
396
516
|
linkedMapArea,
|
|
@@ -399,11 +519,11 @@ function ingestDocs(files, options) {
|
|
|
399
519
|
(0, node_fs_1.writeFileSync)(destination, stampedContent, "utf-8");
|
|
400
520
|
updateMapEntry(repoRoot, destination, linkedEntry);
|
|
401
521
|
}
|
|
402
|
-
if (
|
|
522
|
+
if (docClassification === "doctrine-candidate" && !options.dryRun) {
|
|
403
523
|
emitTelemetry(telPath, runId, {
|
|
404
524
|
event: "doc-auto-promoted",
|
|
405
525
|
file: relDestination,
|
|
406
|
-
classification,
|
|
526
|
+
classification: docClassification,
|
|
407
527
|
linked_map_area: linkedMapArea,
|
|
408
528
|
cluster_id: clusterId,
|
|
409
529
|
});
|
|
@@ -411,7 +531,7 @@ function ingestDocs(files, options) {
|
|
|
411
531
|
emitTelemetry(telPath, runId, {
|
|
412
532
|
event: "docs-ingest",
|
|
413
533
|
file: relSource,
|
|
414
|
-
classification,
|
|
534
|
+
classification: docClassification,
|
|
415
535
|
destination: relDestination,
|
|
416
536
|
linked_map_area: linkedMapArea,
|
|
417
537
|
cluster_id: clusterId,
|
|
@@ -457,12 +577,16 @@ function ingestDocs(files, options) {
|
|
|
457
577
|
results.push({
|
|
458
578
|
sourcePath: relSource,
|
|
459
579
|
destinationPath: relDestination,
|
|
460
|
-
classification,
|
|
580
|
+
classification: docClassification,
|
|
461
581
|
linkedMapArea,
|
|
462
582
|
runId,
|
|
463
583
|
dryRun: Boolean(options.dryRun),
|
|
464
584
|
nearestSummary,
|
|
465
585
|
summaryDeltaWarranted: summaryDelta.updateWarranted,
|
|
586
|
+
routingDecision: routingDecision.outcome,
|
|
587
|
+
reviewPacket: routingDecision.reviewPacket
|
|
588
|
+
? { ...routingDecision.reviewPacket, sourcePath: relSource, proposedDestination: relDestination, conflicts: [] }
|
|
589
|
+
: undefined,
|
|
466
590
|
});
|
|
467
591
|
}
|
|
468
592
|
emitTelemetry(telPath, runId, {
|
|
@@ -470,6 +594,15 @@ function ingestDocs(files, options) {
|
|
|
470
594
|
count: results.length,
|
|
471
595
|
cluster_id: clusterId,
|
|
472
596
|
});
|
|
597
|
+
// Write review queue if any review-required results exist
|
|
598
|
+
const reviewPackets = results
|
|
599
|
+
.filter(r => r.routingDecision === "review-required" && r.reviewPacket)
|
|
600
|
+
.map(r => r.reviewPacket);
|
|
601
|
+
if (reviewPackets.length > 0) {
|
|
602
|
+
// Apply any prior human decisions before writing
|
|
603
|
+
const mergedPackets = (0, index_js_1.applyReviewDecisions)(reviewPackets, priorQueue);
|
|
604
|
+
(0, index_js_1.writeReviewQueue)(mergedPackets, runId, rawDir);
|
|
605
|
+
}
|
|
473
606
|
if (!options.dryRun) {
|
|
474
607
|
writeDocsIngestState(repoRoot, {
|
|
475
608
|
run_id: runId,
|