@lsctech/polaris 0.4.8 → 0.5.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.
@@ -195,6 +195,10 @@ function compileStartupPacket(input) {
195
195
  prompt_mode: 'full',
196
196
  prompt_metrics: { mode: 'full', char_count: 0, estimated_tokens: 0 },
197
197
  role_context: roleContextForWorkerRole('startup'),
198
+ routing_context: {
199
+ task_type: "startup",
200
+ required_capabilities: ["orchestration"],
201
+ },
198
202
  result_file_contract: {
199
203
  result_file: input.resultFile,
200
204
  result_required_fields: Object.fromEntries([
@@ -287,6 +291,10 @@ function compileImplPacket(input) {
287
291
  prompt_mode: promptMode,
288
292
  prompt_metrics: promptResult.metrics,
289
293
  role_context: roleContextForWorkerRole('impl'),
294
+ routing_context: {
295
+ task_type: "impl",
296
+ required_capabilities: ["implementation"],
297
+ },
290
298
  prohibited_write_paths: exports.WORKER_PROHIBITED_WRITE_PATHS,
291
299
  result_file_contract: {
292
300
  result_file: input.resultFile,
@@ -341,6 +349,10 @@ function compileFinalizePacket(input) {
341
349
  prompt_mode: 'full',
342
350
  prompt_metrics: { mode: 'full', char_count: 0, estimated_tokens: 0 },
343
351
  role_context: roleContextForWorkerRole('finalize'),
352
+ routing_context: {
353
+ task_type: "finalize",
354
+ required_capabilities: ["finalization"],
355
+ },
344
356
  result_file_contract: {
345
357
  result_file: input.resultFile,
346
358
  result_required_fields: Object.fromEntries([
@@ -390,6 +402,10 @@ function compilePreflightPacket(input) {
390
402
  prompt_mode: 'full',
391
403
  prompt_metrics: { mode: 'full', char_count: 0, estimated_tokens: 0 },
392
404
  role_context: roleContextForWorkerRole('preflight'),
405
+ routing_context: {
406
+ task_type: "startup",
407
+ required_capabilities: ["orchestration"],
408
+ },
393
409
  result_file_contract: {
394
410
  result_file: input.resultFile,
395
411
  result_required_fields: Object.fromEntries([
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.selectNextChild = selectNextChild;
4
+ exports.selectChildSlotClaims = selectChildSlotClaims;
4
5
  /**
5
6
  * Select the next child to execute.
6
7
  *
@@ -14,3 +15,83 @@ function selectNextChild(state) {
14
15
  return null;
15
16
  return [...state.open_children].sort()[0] ?? null;
16
17
  }
18
+ function countActiveSlotsByProvider(claims) {
19
+ const activeSlotsByProvider = {};
20
+ for (const claim of claims) {
21
+ if (!claim.provider)
22
+ continue;
23
+ activeSlotsByProvider[claim.provider] = (activeSlotsByProvider[claim.provider] ?? 0) + 1;
24
+ }
25
+ return activeSlotsByProvider;
26
+ }
27
+ function selectChildSlotClaims(args) {
28
+ const now = args.now ?? new Date();
29
+ const nowMs = now.getTime();
30
+ const openSet = new Set(args.open_children);
31
+ const completedSet = new Set(args.completed_children);
32
+ const expired_claims = [];
33
+ const retainedClaims = args.existing_claims.filter((claim) => {
34
+ const expiresAt = new Date(claim.expires_at).getTime();
35
+ const notExpired = Number.isFinite(expiresAt) && expiresAt > nowMs;
36
+ const stillOpen = openSet.has(claim.child_id);
37
+ if (!notExpired || !stillOpen) {
38
+ expired_claims.push(claim.child_id);
39
+ return false;
40
+ }
41
+ return true;
42
+ });
43
+ const rejected_children = {};
44
+ const claimedChildren = new Set(retainedClaims.map((claim) => claim.child_id));
45
+ const availableSlots = Math.max(0, args.max_concurrent - retainedClaims.length);
46
+ const newClaims = [];
47
+ if (availableSlots > 0) {
48
+ for (const childId of args.open_children) {
49
+ if (newClaims.length >= availableSlots)
50
+ break;
51
+ if (childId === args.active_child)
52
+ continue;
53
+ if (claimedChildren.has(childId))
54
+ continue;
55
+ const dependencies = args.get_dependencies(childId);
56
+ const unmetDependencies = dependencies.filter((dependency) => openSet.has(dependency) && !completedSet.has(dependency));
57
+ if (unmetDependencies.length > 0) {
58
+ rejected_children[childId] = "blocked-dependency";
59
+ continue;
60
+ }
61
+ const decision = args.decide_route({
62
+ childId,
63
+ activeSlotsByProvider: countActiveSlotsByProvider([...retainedClaims, ...newClaims]),
64
+ });
65
+ const hardIneligibleReasons = new Set([
66
+ "role-disabled",
67
+ "not-in-policy",
68
+ "quota-exhausted",
69
+ "trust-too-low",
70
+ "capability-mismatch",
71
+ "cost-policy",
72
+ "no-slot",
73
+ ]);
74
+ const routingExhausted = decision.exhaustedReason !== undefined &&
75
+ (hardIneligibleReasons.has(decision.exhaustedReason) || decision.mode !== "delegated");
76
+ const canClaim = decision.selectedProvider !== undefined || decision.mode === "delegated";
77
+ if (routingExhausted || !canClaim) {
78
+ rejected_children[childId] = "router-ineligible";
79
+ continue;
80
+ }
81
+ newClaims.push({
82
+ child_id: childId,
83
+ provider: decision.selectedProvider ?? null,
84
+ claimed_at: now.toISOString(),
85
+ expires_at: new Date(nowMs + args.claim_ttl_ms).toISOString(),
86
+ selection_reason: decision.selectionReason,
87
+ });
88
+ }
89
+ }
90
+ const slot_claims = [...retainedClaims, ...newClaims];
91
+ return {
92
+ selected_child: slot_claims[0]?.child_id ?? null,
93
+ slot_claims,
94
+ rejected_children,
95
+ expired_claims,
96
+ };
97
+ }
@@ -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.5.0",
4
4
  "description": "Polaris — standalone taskchain orchestration framework for governed AI agent workflows",
5
5
  "bin": {
6
6
  "polaris": "dist/cli/index.js",