@lsctech/polaris 0.4.8 → 0.4.9

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.
@@ -4,6 +4,7 @@ exports.CANDIDATE_MARKER = void 0;
4
4
  exports.parseFrontMatter = parseFrontMatter;
5
5
  exports.addCandidateGovernanceMetadata = addCandidateGovernanceMetadata;
6
6
  exports.stampIngestFrontMatter = stampIngestFrontMatter;
7
+ exports.deriveTypeFromDirectory = deriveTypeFromDirectory;
7
8
  exports.backfillOkfType = backfillOkfType;
8
9
  exports.doctrineDraft = doctrineDraft;
9
10
  exports.doctrinePromote = doctrinePromote;
@@ -212,13 +213,102 @@ function addMissingFrontMatterField(content, key, value) {
212
213
  }
213
214
  return `---\n${key}: ${value}\n---\n\n${normalized}`;
214
215
  }
216
+ /**
217
+ * Sets a frontmatter field to a new value, replacing it if present or adding
218
+ * it if absent. Unlike `addMissingFrontMatterField`, this always applies the
219
+ * given value — used when a lifecycle transition (promote/deprecate) must
220
+ * make `type` agree with the doc's new directory tier, overriding whatever
221
+ * value it carried in its previous tier.
222
+ */
223
+ function setFrontMatterField(content, key, value) {
224
+ const normalized = content.replace(/\r\n/g, "\n");
225
+ if (normalized.startsWith("---\n")) {
226
+ const end = normalized.indexOf("\n---", 4);
227
+ if (end !== -1) {
228
+ const frontMatter = normalized.slice(4, end);
229
+ const afterFrontMatter = normalized.slice(end + 4);
230
+ const lowerKey = key.toLowerCase();
231
+ let found = false;
232
+ const newLines = frontMatter.split("\n").map((l) => {
233
+ const colonIdx = l.indexOf(":");
234
+ if (colonIdx !== -1 && l.slice(0, colonIdx).trim().toLowerCase() === lowerKey) {
235
+ found = true;
236
+ return `${key}: ${value}`;
237
+ }
238
+ return l;
239
+ });
240
+ if (found) {
241
+ return `---\n${newLines.join("\n")}\n---${afterFrontMatter}`;
242
+ }
243
+ return `---\n${frontMatter}\n${key}: ${value}\n---${afterFrontMatter}`;
244
+ }
245
+ }
246
+ return `---\n${key}: ${value}\n---\n\n${normalized}`;
247
+ }
248
+ /**
249
+ * Directory-tier defaults for `type`, keyed by the first two path segments
250
+ * relative to `smartdocs/`. Mirrors the lifecycle-tier vocabulary already
251
+ * used by `ingest.ts`'s `DocsClassification`, since directory position — not
252
+ * frontmatter — is the authoritative signal for a doc's lifecycle state
253
+ * (docs-authority-model.md "Key rule"). Falling back to a literal `"raw"`
254
+ * for every file regardless of directory (the prior behavior) let a doc
255
+ * sitting in `doctrine/active/` end up with `type: raw`, contradicting its
256
+ * own directory-encoded authority tier.
257
+ */
258
+ const DIRECTORY_TYPE_DEFAULTS = {
259
+ "raw": "raw",
260
+ "doctrine/active": "doctrine",
261
+ "doctrine/candidate": "doctrine-candidate",
262
+ "doctrine/deprecated": "doctrine-deprecated",
263
+ "doctrine": "doctrine",
264
+ "specs/raw": "raw",
265
+ "specs/active": "spec",
266
+ "specs/implemented": "spec-implemented",
267
+ "specs/superseded": "spec-superseded",
268
+ "specs/archive": "spec-archive",
269
+ "specs": "spec",
270
+ "audits/findings": "audit-finding",
271
+ "audits/resolved": "audit-resolved",
272
+ "audits": "audit",
273
+ "decisions": "decision",
274
+ "architecture": "architecture",
275
+ "integrations": "integration",
276
+ "medic": "medic",
277
+ "runtime/run-reports": "run-report",
278
+ "runtime/summaries": "runtime-summary",
279
+ "runtime": "runtime",
280
+ };
281
+ /**
282
+ * Derives a directory-tier-appropriate `type` default for a SmartDocs file
283
+ * that has neither `kind` nor `doc-type` set. `relPath` is relative to
284
+ * `smartdocs/` (forward-slash separated). Reserved front-door filenames
285
+ * (`POLARIS.md`, `SUMMARY.md`, `index.md`) get `"index"` regardless of tier,
286
+ * since they're navigational scaffolding, not tier content.
287
+ */
288
+ function deriveTypeFromDirectory(relPath) {
289
+ const name = (0, node_path_1.basename)(relPath);
290
+ if (name === "POLARIS.md" || name === "SUMMARY.md" || name === "index.md") {
291
+ return "index";
292
+ }
293
+ const segments = relPath.split("/").slice(0, -1);
294
+ if (segments.length >= 2) {
295
+ const twoLevel = `${segments[0]}/${segments[1]}`;
296
+ if (DIRECTORY_TYPE_DEFAULTS[twoLevel])
297
+ return DIRECTORY_TYPE_DEFAULTS[twoLevel];
298
+ }
299
+ if (segments.length >= 1 && DIRECTORY_TYPE_DEFAULTS[segments[0]]) {
300
+ return DIRECTORY_TYPE_DEFAULTS[segments[0]];
301
+ }
302
+ return "raw";
303
+ }
215
304
  /**
216
305
  * Retrofit existing SmartDocs with an OKF-conformant `type` frontmatter field.
217
306
  *
218
307
  * Walks every `.md` file under `smartdocs/`. For each file already missing a
219
308
  * `type` key: derives the value from `kind` if present, else `doc-type` if
220
- * present, else defaults to `"raw"`. Files that already declare `type` are
221
- * left untouched and reported in `skipped`.
309
+ * present, else derives a directory-tier-appropriate default via
310
+ * `deriveTypeFromDirectory` (see that function for the tier mapping). Files
311
+ * that already declare `type` are left untouched and reported in `skipped`.
222
312
  *
223
313
  * This is the retroactive counterpart to `addCandidateGovernanceMetadata`,
224
314
  * which stamps `type` on newly-governed candidate docs going forward — this
@@ -264,7 +354,8 @@ function backfillOkfType(repoRoot, options = {}) {
264
354
  result.skipped.push(filePath);
265
355
  return;
266
356
  }
267
- const derivedType = fm.kind ?? fm["doc-type"] ?? "raw";
357
+ const relPath = (0, node_path_1.relative)(smartdocsDir, filePath).replace(/\\/g, "/");
358
+ const derivedType = fm.kind ?? fm["doc-type"] ?? deriveTypeFromDirectory(relPath);
268
359
  if (!options.dryRun) {
269
360
  (0, node_fs_1.writeFileSync)(filePath, addMissingFrontMatterField(content, "type", derivedType), "utf-8");
270
361
  }
@@ -437,7 +528,8 @@ function doctrinePromote(path, options) {
437
528
  if ((0, node_fs_1.existsSync)(destination)) {
438
529
  throw new Error(`Destination already exists: ${destination}`);
439
530
  }
440
- const activeContent = content.replace(`${exports.CANDIDATE_MARKER}\n`, "").replace(exports.CANDIDATE_MARKER, "");
531
+ const strippedContent = content.replace(`${exports.CANDIDATE_MARKER}\n`, "").replace(exports.CANDIDATE_MARKER, "");
532
+ const activeContent = setFrontMatterField(strippedContent, "type", "doctrine");
441
533
  (0, node_fs_1.writeFileSync)(destination, activeContent, "utf-8");
442
534
  (0, node_fs_1.unlinkSync)(source);
443
535
  // Move co-located provenance sidecar if present
@@ -501,7 +593,8 @@ function doctrineDeprecate(path, options) {
501
593
  }
502
594
  const content = (0, node_fs_1.readFileSync)(source, "utf-8");
503
595
  const deprecatedAt = new Date().toISOString();
504
- const deprecatedContent = `<!-- polaris:doctrine-deprecated deprecatedAt="${deprecatedAt}" runId="${runId}" -->\n${content}`;
596
+ const retypedContent = setFrontMatterField(content, "type", "doctrine-deprecated");
597
+ const deprecatedContent = `<!-- polaris:doctrine-deprecated deprecatedAt="${deprecatedAt}" runId="${runId}" -->\n${retypedContent}`;
505
598
  (0, node_fs_1.writeFileSync)(destination, deprecatedContent, "utf-8");
506
599
  (0, node_fs_1.unlinkSync)(source);
507
600
  // Move co-located provenance sidecar if present
@@ -834,7 +927,9 @@ function specPromote(path, options) {
834
927
  if ((0, node_fs_1.existsSync)(destination)) {
835
928
  throw new Error(`Destination already exists: ${destination}`);
836
929
  }
837
- (0, node_fs_1.renameSync)(source, destination);
930
+ const retypedContent = setFrontMatterField(content, "type", "spec");
931
+ (0, node_fs_1.writeFileSync)(destination, retypedContent, "utf-8");
932
+ (0, node_fs_1.unlinkSync)(source);
838
933
  if ((0, node_fs_1.existsSync)(provenanceSrcPath)) {
839
934
  (0, node_fs_1.renameSync)(provenanceSrcPath, destination.replace(/\.md$/, ".provenance.json"));
840
935
  }
@@ -456,8 +456,14 @@ function ingestDocs(files, options) {
456
456
  };
457
457
  const routingDecision = (0, index_js_1.route)(classificationResult, thresholds);
458
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
+ // A prior human/agent review decision or a scoped --approve-authority call
460
+ // overrides an otherwise-review-required outcome so the placement it
461
+ // decided on actually happens.
462
+ const priorDecision = priorQueue.find((p) => p.sourcePath === relSource)?.reviewDecision;
463
+ const forcedApprove = routingDecision.outcome === "review-required" &&
464
+ (priorDecision === "approve" || Boolean(options.approveAuthority));
459
465
  // review-required: leave in raw/, emit packet, skip move
460
- if (routingDecision.outcome === "review-required") {
466
+ if (routingDecision.outcome === "review-required" && !forcedApprove) {
461
467
  const packet = {
462
468
  ...routingDecision.reviewPacket,
463
469
  sourcePath: relSource,
@@ -504,6 +510,16 @@ function ingestDocs(files, options) {
504
510
  linked_map_area: linkedMapArea,
505
511
  cluster_id: clusterId,
506
512
  });
513
+ if (forcedApprove) {
514
+ emitTelemetry(telPath, runId, {
515
+ event: "docs-ingest-approved-override",
516
+ file: relSource,
517
+ classification: docClassification,
518
+ destination: relDestination,
519
+ reason: priorDecision === "approve" ? "prior-review-decision" : "approve-authority-flag",
520
+ cluster_id: clusterId,
521
+ });
522
+ }
507
523
  if (!options.dryRun) {
508
524
  if ((0, node_path_1.resolve)(absSource) !== (0, node_path_1.resolve)(destination)) {
509
525
  (0, node_fs_1.renameSync)(absSource, destination);
@@ -583,7 +599,7 @@ function ingestDocs(files, options) {
583
599
  dryRun: Boolean(options.dryRun),
584
600
  nearestSummary,
585
601
  summaryDeltaWarranted: summaryDelta.updateWarranted,
586
- routingDecision: routingDecision.outcome,
602
+ routingDecision: forcedApprove ? "approved-override" : routingDecision.outcome,
587
603
  reviewPacket: routingDecision.reviewPacket
588
604
  ? { ...routingDecision.reviewPacket, sourcePath: relSource, proposedDestination: relDestination, conflicts: [] }
589
605
  : undefined,
@@ -11,6 +11,7 @@ const smartdoc_ignore_js_1 = require("./smartdoc-ignore.js");
11
11
  const ALLOWED_BASENAMES = new Set([
12
12
  "README.md",
13
13
  "POLARIS.md",
14
+ "POLARIS_RULES.md",
14
15
  "SUMMARY.md",
15
16
  "CHANGELOG.md",
16
17
  "LICENSE.md",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lsctech/polaris",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "Polaris — standalone taskchain orchestration framework for governed AI agent workflows",
5
5
  "bin": {
6
6
  "polaris": "dist/cli/index.js",