@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.
- package/dist/cli/adopt-command.js +2 -1
- package/dist/cli/adopt-genesis.js +17 -7
- package/dist/cli/adopt-smartdocs.js +70 -30
- package/dist/cognition/librarian-packet.js +16 -7
- package/dist/loop/adapters/cli-subtask-bridge.js +1 -0
- package/dist/loop/adapters/terminal-cli.js +1 -0
- package/dist/loop/body-parser.js +40 -1
- package/dist/loop/continue.js +71 -0
- package/dist/smartdocs-engine/canon-check.js +80 -0
- package/dist/smartdocs-engine/doctrine.js +231 -0
- package/dist/smartdocs-engine/index.js +105 -22
- package/dist/smartdocs-engine/seed-instructions.js +210 -0
- package/dist/tracker/adapters/linear/index.js +106 -11
- package/package.json +1 -1
|
@@ -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,
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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.
|