@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.
@@ -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, options) => {
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/active`,
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[classification]);
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: classification,
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 (classification === "doctrine-candidate" && !options.dryRun) {
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lsctech/polaris",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Polaris — standalone taskchain orchestration framework for governed AI agent workflows",
5
5
  "bin": {
6
6
  "polaris-cli": "dist/cli/index.js"