@lsctech/polaris 0.4.4 → 0.4.6

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,15 +4,20 @@ exports.DRAFT_MARKER = void 0;
4
4
  exports.hasDraftMarker = hasDraftMarker;
5
5
  exports.generateDraft = generateDraft;
6
6
  exports.generateSummaryDraft = generateSummaryDraft;
7
+ exports.generateBundleRootIndex = generateBundleRootIndex;
8
+ exports.generateDirectoryIndex = generateDirectoryIndex;
7
9
  exports.seedInstructions = seedInstructions;
8
10
  exports.seedSummary = seedSummary;
9
11
  exports.seedInstructionsAll = seedInstructionsAll;
10
12
  exports.seedSummaryAll = seedSummaryAll;
13
+ exports.seedIndex = seedIndex;
14
+ exports.seedIndexAll = seedIndexAll;
11
15
  const node_fs_1 = require("node:fs");
12
16
  const node_path_1 = require("node:path");
13
17
  const loader_js_1 = require("../config/loader.js");
14
18
  const atlas_js_1 = require("../map/atlas.js");
15
19
  const smartdoc_ignore_js_1 = require("./smartdoc-ignore.js");
20
+ const doctrine_js_1 = require("./doctrine.js");
16
21
  exports.DRAFT_MARKER = "<!-- polaris:draft -->";
17
22
  function hasDraftMarker(filePath) {
18
23
  try {
@@ -176,6 +181,136 @@ function generateSummaryDraft(targetDir, repoRoot, _allRoutes) {
176
181
  ];
177
182
  return lines.join("\n");
178
183
  }
184
+ const RESERVED_INDEX_NAMES = new Set(["index.md", "POLARIS.md", "SUMMARY.md", "log.md"]);
185
+ function isReservedIndexName(name) {
186
+ return RESERVED_INDEX_NAMES.has(name);
187
+ }
188
+ function listConceptFiles(dir) {
189
+ try {
190
+ return (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })
191
+ .filter((e) => e.isFile() && e.name.endsWith(".md") && !isReservedIndexName(e.name))
192
+ .map((e) => (0, node_path_1.join)(dir, e.name))
193
+ .sort();
194
+ }
195
+ catch {
196
+ return [];
197
+ }
198
+ }
199
+ function conceptLabel(filePath) {
200
+ try {
201
+ const content = (0, node_fs_1.readFileSync)(filePath, "utf-8");
202
+ const fm = (0, doctrine_js_1.parseFrontMatter)(content);
203
+ const base = (0, node_path_1.basename)(filePath);
204
+ return fm.description || fm.title || base.replace(/\.md$/, "");
205
+ }
206
+ catch {
207
+ return (0, node_path_1.basename)(filePath).replace(/\.md$/, "");
208
+ }
209
+ }
210
+ function collectSmartDocsDirs(dir, root, result = []) {
211
+ try {
212
+ const rel = (0, node_path_1.relative)(root, dir).replace(/\\/g, "/");
213
+ if (rel.split("/").some((s) => s === "raw" || s.startsWith("."))) {
214
+ return result;
215
+ }
216
+ result.push(rel);
217
+ for (const entry of (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })) {
218
+ if (!entry.isDirectory())
219
+ continue;
220
+ if (entry.name.startsWith(".") || entry.name === "raw")
221
+ continue;
222
+ const full = (0, node_path_1.join)(dir, entry.name);
223
+ collectSmartDocsDirs(full, root, result);
224
+ }
225
+ }
226
+ catch {
227
+ // ignore unreadable dirs
228
+ }
229
+ return result;
230
+ }
231
+ function generateBundleRootIndex(repoRoot, allRoutes) {
232
+ const lines = [
233
+ "---",
234
+ "okf_version: \"0.1\"",
235
+ "---",
236
+ "",
237
+ exports.DRAFT_MARKER,
238
+ "# SmartDocs — Polaris Cognition Bundle",
239
+ "",
240
+ "> Polaris draft — review and remove the `<!-- polaris:draft -->` marker to promote.",
241
+ "",
242
+ "## Governance",
243
+ "",
244
+ ];
245
+ const doctrineDir = (0, node_path_1.join)(repoRoot, "smartdocs", "doctrine", "active");
246
+ const doctrineFiles = listConceptFiles(doctrineDir);
247
+ if (doctrineFiles.length > 0) {
248
+ for (const file of doctrineFiles) {
249
+ const relPath = (0, node_path_1.relative)((0, node_path_1.join)(repoRoot, "smartdocs"), file).replace(/\\/g, "/");
250
+ lines.push(`- [${conceptLabel(file)}](${relPath})`);
251
+ }
252
+ }
253
+ else {
254
+ lines.push("- [Doctrine — Active](doctrine/active/)");
255
+ }
256
+ lines.push("");
257
+ lines.push("## Specs", "");
258
+ const specsDir = (0, node_path_1.join)(repoRoot, "smartdocs", "specs", "active");
259
+ const specsFiles = listConceptFiles(specsDir);
260
+ if (specsFiles.length > 0) {
261
+ for (const file of specsFiles) {
262
+ const relPath = (0, node_path_1.relative)((0, node_path_1.join)(repoRoot, "smartdocs"), file).replace(/\\/g, "/");
263
+ lines.push(`- [${conceptLabel(file)}](${relPath})`);
264
+ }
265
+ }
266
+ else {
267
+ lines.push("- [Specs — Active](specs/active/)");
268
+ }
269
+ lines.push("");
270
+ lines.push("## Routes", "");
271
+ const instructionFiles = [
272
+ ...new Set(Object.values(allRoutes).map((e) => e.instructionFile).filter(Boolean)),
273
+ ].sort();
274
+ if (instructionFiles.length > 0) {
275
+ for (const file of instructionFiles) {
276
+ lines.push(`- [${file}](../${file})`);
277
+ }
278
+ }
279
+ else {
280
+ lines.push("<!-- Route POLARIS.md files will be listed here from atlas signals. -->");
281
+ }
282
+ lines.push("");
283
+ return lines.join("\n");
284
+ }
285
+ function generateDirectoryIndex(targetDir, repoRoot) {
286
+ const absDir = (0, node_path_1.resolve)(repoRoot, targetDir);
287
+ const dirLabel = (0, node_path_1.basename)(absDir) || "SmartDocs";
288
+ const files = listConceptFiles(absDir);
289
+ const lines = [
290
+ "---",
291
+ "okf_version: \"0.1\"",
292
+ "---",
293
+ "",
294
+ exports.DRAFT_MARKER,
295
+ `# ${dirLabel}`,
296
+ "",
297
+ "> Polaris draft — review and remove the `<!-- polaris:draft -->` marker to promote.",
298
+ "",
299
+ "## Concepts",
300
+ "",
301
+ ];
302
+ if (files.length > 0) {
303
+ for (const file of files) {
304
+ const relPath = (0, node_path_1.relative)(absDir, file).replace(/\\/g, "/");
305
+ lines.push(`- [${conceptLabel(file)}](${relPath})`);
306
+ }
307
+ }
308
+ else {
309
+ lines.push("<!-- No concept files in this directory yet. -->");
310
+ }
311
+ lines.push("");
312
+ return lines.join("\n");
313
+ }
179
314
  function seedInstructions(targetPath, repoRoot, opts = {}) {
180
315
  const absTarget = (0, node_path_1.resolve)(repoRoot, targetPath);
181
316
  // Path traversal check: ensure absTarget is within repoRoot
@@ -333,3 +468,78 @@ function seedSummaryAll(repoRoot, opts = {}) {
333
468
  }
334
469
  return { written, skippedExists, skippedDraft, skippedIneligible, skippedRoot };
335
470
  }
471
+ function seedIndex(targetPath, repoRoot, opts = {}) {
472
+ const absTarget = (0, node_path_1.resolve)(repoRoot, targetPath);
473
+ const relCheck = (0, node_path_1.relative)(repoRoot, absTarget);
474
+ if (relCheck.startsWith("..") || relCheck.startsWith("/")) {
475
+ throw new Error(`Path traversal detected: target path is outside repo root`);
476
+ }
477
+ // Reject targets outside smartdocs/
478
+ const relTarget = relCheck.replace(/\\/g, "/");
479
+ if (!relTarget.startsWith("smartdocs/") && relTarget !== "smartdocs") {
480
+ throw new Error(`seedIndex only writes index.md under smartdocs/; rejecting target: ${relTarget}`);
481
+ }
482
+ const outFile = (0, node_path_1.join)(absTarget, "index.md");
483
+ if ((0, node_fs_1.existsSync)(outFile)) {
484
+ if (hasDraftMarker(outFile)) {
485
+ return "skipped-draft";
486
+ }
487
+ return "skipped-exists";
488
+ }
489
+ const config = (0, loader_js_1.loadConfig)(repoRoot);
490
+ const atlasPath = (0, node_path_1.resolve)(repoRoot, config.repo.sidecarOutputPath ?? ".polaris/map");
491
+ const allRoutes = {
492
+ ...(0, atlas_js_1.readFileRoutes)(atlasPath),
493
+ ...(0, atlas_js_1.readNeedsReview)(atlasPath),
494
+ };
495
+ const isBundleRoot = relTarget === "smartdocs";
496
+ const content = isBundleRoot
497
+ ? generateBundleRootIndex(repoRoot, allRoutes)
498
+ : generateDirectoryIndex(relTarget, repoRoot);
499
+ if (!opts.dryRun) {
500
+ (0, node_fs_1.writeFileSync)(outFile, content, "utf-8");
501
+ }
502
+ return "written";
503
+ }
504
+ function seedIndexAll(repoRoot, opts = {}) {
505
+ const config = (0, loader_js_1.loadConfig)(repoRoot);
506
+ const atlasPath = (0, node_path_1.resolve)(repoRoot, config.repo.sidecarOutputPath ?? ".polaris/map");
507
+ const allRoutes = {
508
+ ...(0, atlas_js_1.readFileRoutes)(atlasPath),
509
+ ...(0, atlas_js_1.readNeedsReview)(atlasPath),
510
+ };
511
+ const smartdocsRoot = (0, node_path_1.resolve)(repoRoot, "smartdocs");
512
+ if (!(0, node_fs_1.existsSync)(smartdocsRoot)) {
513
+ return { written: [], skippedExists: [], skippedDraft: [], skippedIneligible: [] };
514
+ }
515
+ const dirs = collectSmartDocsDirs(smartdocsRoot, repoRoot);
516
+ const smartdocsRel = "smartdocs";
517
+ if (!dirs.includes(smartdocsRel)) {
518
+ dirs.unshift(smartdocsRel);
519
+ }
520
+ const written = [];
521
+ const skippedExists = [];
522
+ const skippedDraft = [];
523
+ for (const relDir of dirs) {
524
+ const absDir = (0, node_path_1.resolve)(repoRoot, relDir);
525
+ const outFile = (0, node_path_1.join)(absDir, "index.md");
526
+ if ((0, node_fs_1.existsSync)(outFile)) {
527
+ if (hasDraftMarker(outFile)) {
528
+ skippedDraft.push(relDir);
529
+ }
530
+ else {
531
+ skippedExists.push(relDir);
532
+ }
533
+ continue;
534
+ }
535
+ const isBundleRoot = relDir === "smartdocs";
536
+ const content = isBundleRoot
537
+ ? generateBundleRootIndex(repoRoot, allRoutes)
538
+ : generateDirectoryIndex(relDir, repoRoot);
539
+ if (!opts.dryRun) {
540
+ (0, node_fs_1.writeFileSync)(outFile, content, "utf-8");
541
+ }
542
+ written.push(relDir);
543
+ }
544
+ return { written, skippedExists, skippedDraft, skippedIneligible: [] };
545
+ }
@@ -190,6 +190,36 @@ class LinearGraphqlClient {
190
190
  `, { id });
191
191
  return data.issue;
192
192
  }
193
+ async getIssueStateOptions(id) {
194
+ const data = await this.graphql(`
195
+ query PolarisLinearIssueStateOptions($id: String!) {
196
+ issue(id: $id) {
197
+ state { id }
198
+ team {
199
+ states(first: 250) {
200
+ nodes { id name type }
201
+ }
202
+ }
203
+ }
204
+ }
205
+ `, { id });
206
+ if (!data.issue)
207
+ return null;
208
+ return {
209
+ currentStateId: data.issue.state?.id ?? null,
210
+ states: data.issue.team?.states?.nodes ?? [],
211
+ };
212
+ }
213
+ async updateIssueState(id, stateId) {
214
+ const data = await this.graphql(`
215
+ mutation PolarisLinearIssueUpdateState($id: String!, $stateId: String!) {
216
+ issueUpdate(id: $id, input: { stateId: $stateId }) {
217
+ success
218
+ }
219
+ }
220
+ `, { id, stateId });
221
+ return data.issueUpdate.success;
222
+ }
193
223
  }
194
224
  /**
195
225
  * Implements the Linear direct adapter for synchronizing issues.
@@ -530,7 +560,7 @@ class LinearAdapter {
530
560
  * @param evidence - Optional evidence for the transition (e.g., commit hash).
531
561
  * @returns A lifecycle transition result indicating success, skip, or failure.
532
562
  */
533
- async transitionLifecycleState(taskId, lifecycleState, evidence) {
563
+ async transitionLifecycleState(taskId, lifecycleState, _evidence) {
534
564
  if (lifecycleState === "no_status_change") {
535
565
  return {
536
566
  applied: false,
@@ -538,17 +568,82 @@ class LinearAdapter {
538
568
  skipReason: "Lifecycle state is 'no_status_change', skipping transition",
539
569
  };
540
570
  }
541
- // Linear requires team-specific state IDs for state transitions.
542
- // This implementation is a placeholder - full implementation would need:
543
- // 1. Fetch available states for the issue's team
544
- // 2. Map normalized lifecycle state to a specific Linear state ID
545
- // 3. Use GraphQL mutation to update the issue state
546
- console.warn(`LinearAdapter: transitionLifecycleState not fully implemented. Would transition ${taskId} to ${lifecycleState}.`);
547
- return {
548
- applied: false,
549
- skipped: true,
550
- skipReason: "Lifecycle state transitions are not yet implemented for Linear adapter",
571
+ let stateOptions;
572
+ try {
573
+ stateOptions = await this.linearClient.getIssueStateOptions(taskId);
574
+ }
575
+ catch (err) {
576
+ return {
577
+ applied: false,
578
+ skipped: false,
579
+ error: `Failed to fetch Linear workflow states for ${taskId}: ${err instanceof Error ? err.message : String(err)}`,
580
+ };
581
+ }
582
+ if (!stateOptions) {
583
+ return {
584
+ applied: false,
585
+ skipped: true,
586
+ skipReason: `Linear issue '${taskId}' not found`,
587
+ };
588
+ }
589
+ const targetState = this.resolveWorkflowState(stateOptions.states, lifecycleState);
590
+ if (!targetState) {
591
+ return {
592
+ applied: false,
593
+ skipped: true,
594
+ skipReason: `No Linear workflow state on this issue's team maps to lifecycle state '${lifecycleState}'`,
595
+ };
596
+ }
597
+ if (targetState.id === stateOptions.currentStateId) {
598
+ return {
599
+ applied: false,
600
+ skipped: true,
601
+ skipReason: `Issue is already in target state '${targetState.name}'`,
602
+ };
603
+ }
604
+ try {
605
+ const success = await this.linearClient.updateIssueState(taskId, targetState.id);
606
+ if (!success) {
607
+ return {
608
+ applied: false,
609
+ skipped: false,
610
+ error: `Linear issueUpdate mutation for ${taskId} returned success: false`,
611
+ };
612
+ }
613
+ return { applied: true, skipped: false };
614
+ }
615
+ catch (err) {
616
+ return {
617
+ applied: false,
618
+ skipped: false,
619
+ error: err instanceof Error ? err.message : String(err),
620
+ };
621
+ }
622
+ }
623
+ /**
624
+ * Picks the Linear workflow state that best matches a normalized lifecycle state.
625
+ *
626
+ * Prefers exact `type` alignment (Linear's own backlog/unstarted/started/completed/canceled
627
+ * classification) so custom state names don't need to match `mapNativeStatus`'s heuristics;
628
+ * falls back to the first state whose name heuristically maps to the target state.
629
+ */
630
+ resolveWorkflowState(states, lifecycleState) {
631
+ const typeCandidates = {
632
+ backlog: ["backlog", "unstarted"],
633
+ in_progress: ["started"],
634
+ in_review: ["started"],
635
+ done: ["completed"],
636
+ blocked: ["started"],
637
+ cancelled: ["canceled", "cancelled"],
551
638
  };
639
+ const byHeuristic = states.filter((s) => this.mapNativeStatus(s.name).lifecycleState === lifecycleState);
640
+ if (byHeuristic.length === 1)
641
+ return byHeuristic[0];
642
+ const wantedTypes = typeCandidates[lifecycleState] ?? [];
643
+ const byType = (byHeuristic.length > 0 ? byHeuristic : states).filter((s) => wantedTypes.includes(s.type));
644
+ if (byType.length > 0)
645
+ return byType[0];
646
+ return byHeuristic[0] ?? null;
552
647
  }
553
648
  /**
554
649
  * Adds a comment to a Linear issue.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lsctech/polaris",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "Polaris — standalone taskchain orchestration framework for governed AI agent workflows",
5
5
  "bin": {
6
6
  "polaris": "dist/cli/index.js",