@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
|
@@ -140,8 +140,9 @@ async function runAdoptPhase(phase, repoRoot, options = {}) {
|
|
|
140
140
|
if (!options.inventory || !options.plan) {
|
|
141
141
|
throw new Error("consolidate requires inventory and plan");
|
|
142
142
|
}
|
|
143
|
+
const dryRun = options.plan.dry_run ?? options.dryRun ?? false;
|
|
143
144
|
await (0, adopt_smartdocs_js_1.migrateSmartDocs)(options.plan, repoRoot);
|
|
144
|
-
await (0, adopt_genesis_js_1.reconcileAgentFiles)(repoRoot);
|
|
145
|
+
await (0, adopt_genesis_js_1.reconcileAgentFiles)(repoRoot, { dryRun });
|
|
145
146
|
break;
|
|
146
147
|
}
|
|
147
148
|
case "map": {
|
|
@@ -25,7 +25,9 @@ function isAlreadyPointer(content) {
|
|
|
25
25
|
});
|
|
26
26
|
return meaningfulLines.length <= 3 && meaningfulLines.some((line) => line.includes("POLARIS_RULES.md"));
|
|
27
27
|
}
|
|
28
|
-
function appendGenesisProvenance(repoRoot, records) {
|
|
28
|
+
function appendGenesisProvenance(repoRoot, records, dryRun = false) {
|
|
29
|
+
if (dryRun)
|
|
30
|
+
return;
|
|
29
31
|
if (records.length === 0)
|
|
30
32
|
return;
|
|
31
33
|
const provenancePath = (0, node_path_1.join)(repoRoot, ".polaris", "adoption-provenance.json");
|
|
@@ -57,6 +59,7 @@ async function reconcileAgentFiles(repoRoot, options) {
|
|
|
57
59
|
const provenanceRecords = [];
|
|
58
60
|
const now = options?.now ?? new Date();
|
|
59
61
|
const timestamp = now.toISOString();
|
|
62
|
+
const dryRun = options?.dryRun ?? false;
|
|
60
63
|
for (const file of AGENT_FILES) {
|
|
61
64
|
const filePath = (0, node_path_1.join)(repoRoot, file);
|
|
62
65
|
if (!(0, node_fs_1.existsSync)(filePath)) {
|
|
@@ -74,6 +77,11 @@ async function reconcileAgentFiles(repoRoot, options) {
|
|
|
74
77
|
});
|
|
75
78
|
continue;
|
|
76
79
|
}
|
|
80
|
+
if (dryRun) {
|
|
81
|
+
process.stdout.write(`[dry-run] would prompt to compress and archive ${file} as genesis doctrine\n`);
|
|
82
|
+
results.push({ file, outcome: "skipped" });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
77
85
|
const rl = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
78
86
|
let answer;
|
|
79
87
|
try {
|
|
@@ -146,11 +154,13 @@ async function reconcileAgentFiles(repoRoot, options) {
|
|
|
146
154
|
const dateStr = formatDate(now);
|
|
147
155
|
const genesisPath = `smartdocs/doctrine/active/${dateStr}-genesis-agent-doctrine.md`;
|
|
148
156
|
const genesisFullPath = (0, node_path_1.join)(repoRoot, genesisPath);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
if (!dryRun) {
|
|
158
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.join)(repoRoot, "smartdocs/doctrine/active"), { recursive: true });
|
|
159
|
+
(0, node_fs_1.writeFileSync)(genesisFullPath, distilled, "utf8");
|
|
160
|
+
const thinPointer = `# Agent Instructions\n\nRead [POLARIS_RULES.md](POLARIS_RULES.md) before beginning any work.\n\n` +
|
|
161
|
+
`<!-- genesis doctrine archived: ${genesisPath} -->\n`;
|
|
162
|
+
(0, node_fs_1.writeFileSync)(filePath, thinPointer, "utf8");
|
|
163
|
+
}
|
|
154
164
|
results.push({ file, outcome: "compressed", genesisPath });
|
|
155
165
|
provenanceRecords.push({
|
|
156
166
|
source_path: file,
|
|
@@ -160,6 +170,6 @@ async function reconcileAgentFiles(repoRoot, options) {
|
|
|
160
170
|
migration_outcome: "compressed",
|
|
161
171
|
});
|
|
162
172
|
}
|
|
163
|
-
appendGenesisProvenance(repoRoot, provenanceRecords);
|
|
173
|
+
appendGenesisProvenance(repoRoot, provenanceRecords, dryRun);
|
|
164
174
|
return results;
|
|
165
175
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scaffoldBundleRoot = scaffoldBundleRoot;
|
|
3
4
|
exports.migrateSmartDocs = migrateSmartDocs;
|
|
4
5
|
const node_child_process_1 = require("node:child_process");
|
|
5
6
|
const node_fs_1 = require("node:fs");
|
|
@@ -110,64 +111,100 @@ function normalizeStatus(step, status, completedAt) {
|
|
|
110
111
|
error: status === "completed" || status === "skipped" ? undefined : step.error,
|
|
111
112
|
};
|
|
112
113
|
}
|
|
114
|
+
const SMARTDOCS_BUNDLE_INDEX_CONTENT = `---\nokf_version: "0.1"\n---\n\n# SmartDocs — Polaris Cognition Bundle\n\n# Governance\n\n- [Docs authority model](specs/active/docs-authority-model.md)\n- [Doctrine](doctrine/active/)\n\n# Routes\n\n<!-- Routes placeholder: dynamic file-routes.json generation deferred. -->\n`;
|
|
115
|
+
const SMARTDOCS_BUNDLE_LOG_CONTENT = "# SmartDocs — Change Log\n";
|
|
116
|
+
function scaffoldBundleRoot(repoRoot, dryRun = false) {
|
|
117
|
+
const smartdocsDir = (0, node_path_1.join)(repoRoot, "smartdocs");
|
|
118
|
+
if (dryRun) {
|
|
119
|
+
if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(smartdocsDir, "index.md"))) {
|
|
120
|
+
process.stdout.write(`[dry-run] would create smartdocs/index.md\n`);
|
|
121
|
+
}
|
|
122
|
+
if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(smartdocsDir, "log.md"))) {
|
|
123
|
+
process.stdout.write(`[dry-run] would create smartdocs/log.md\n`);
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
(0, node_fs_1.mkdirSync)(smartdocsDir, { recursive: true });
|
|
128
|
+
const indexPath = (0, node_path_1.join)(smartdocsDir, "index.md");
|
|
129
|
+
if (!(0, node_fs_1.existsSync)(indexPath)) {
|
|
130
|
+
(0, node_fs_1.writeFileSync)(indexPath, SMARTDOCS_BUNDLE_INDEX_CONTENT, "utf-8");
|
|
131
|
+
}
|
|
132
|
+
const logPath = (0, node_path_1.join)(smartdocsDir, "log.md");
|
|
133
|
+
if (!(0, node_fs_1.existsSync)(logPath)) {
|
|
134
|
+
(0, node_fs_1.writeFileSync)(logPath, SMARTDOCS_BUNDLE_LOG_CONTENT, "utf-8");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
113
137
|
function migrateSmartDocs(plan, repoRoot = (0, node_path_1.resolve)(process.cwd())) {
|
|
114
138
|
const effectivePlan = loadPlan(repoRoot, plan);
|
|
115
139
|
const provenanceRecords = [];
|
|
116
140
|
const seenRecords = existingRecordKeys(repoRoot);
|
|
117
141
|
const now = new Date().toISOString();
|
|
142
|
+
const dryRun = effectivePlan.dry_run ?? false;
|
|
118
143
|
for (let index = 0; index < effectivePlan.steps.length; index += 1) {
|
|
119
144
|
const step = effectivePlan.steps[index];
|
|
120
145
|
if (step.category !== "smartdocs-migrate" || COMPLETE_STATUSES.has(step.status)) {
|
|
121
146
|
continue;
|
|
122
147
|
}
|
|
123
148
|
if (step.routing !== undefined && step.routing !== "candidate") {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
149
|
+
if (!dryRun) {
|
|
150
|
+
effectivePlan.steps[index] = {
|
|
151
|
+
...step,
|
|
152
|
+
status: "skipped",
|
|
153
|
+
completed_at: now,
|
|
154
|
+
error: `routing not candidate: ${step.routing}`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
130
157
|
continue;
|
|
131
158
|
}
|
|
132
159
|
const sourcePath = step.source_path ?? "";
|
|
133
160
|
const destPath = step.dest_path ?? `smartdocs/raw/${(0, node_path_1.basename)(sourcePath || `step-${step.order}.md`)}`;
|
|
134
161
|
if (!sourcePath) {
|
|
135
|
-
|
|
162
|
+
if (!dryRun)
|
|
163
|
+
effectivePlan.steps[index] = normalizeStatus(step, "skipped", now);
|
|
136
164
|
continue;
|
|
137
165
|
}
|
|
138
166
|
if (isExcludedSourcePath(sourcePath)) {
|
|
139
|
-
|
|
167
|
+
if (!dryRun)
|
|
168
|
+
effectivePlan.steps[index] = normalizeStatus(step, "skipped", now);
|
|
140
169
|
continue;
|
|
141
170
|
}
|
|
142
171
|
const sourceAbs = (0, node_path_1.join)(repoRoot, sourcePath);
|
|
143
172
|
const destAbs = (0, node_path_1.join)(repoRoot, destPath);
|
|
144
|
-
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(destAbs), { recursive: true });
|
|
145
173
|
if (!(0, node_fs_1.existsSync)(sourceAbs)) {
|
|
146
174
|
if ((0, node_fs_1.existsSync)(destAbs)) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
175
|
+
if (!dryRun) {
|
|
176
|
+
effectivePlan.steps[index] = normalizeStatus(step, "completed", step.completed_at ?? now);
|
|
177
|
+
const record = {
|
|
178
|
+
step_id: step.step_id,
|
|
179
|
+
source_path: sourcePath,
|
|
180
|
+
dest_path: destPath,
|
|
181
|
+
migrated_at: step.completed_at ?? now,
|
|
182
|
+
migration_run_id: effectivePlan.plan_id,
|
|
183
|
+
transport: "reconciled",
|
|
184
|
+
};
|
|
185
|
+
if (!seenRecords.has(recordKey(record))) {
|
|
186
|
+
provenanceRecords.push(record);
|
|
187
|
+
seenRecords.add(recordKey(record));
|
|
188
|
+
}
|
|
159
189
|
}
|
|
160
190
|
}
|
|
161
191
|
else {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
192
|
+
if (!dryRun) {
|
|
193
|
+
effectivePlan.steps[index] = {
|
|
194
|
+
...step,
|
|
195
|
+
status: "skipped",
|
|
196
|
+
completed_at: now,
|
|
197
|
+
error: `source missing: ${sourcePath}`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
168
200
|
}
|
|
169
201
|
continue;
|
|
170
202
|
}
|
|
203
|
+
if (dryRun) {
|
|
204
|
+
process.stdout.write(`[dry-run] would move ${sourcePath} → ${destPath}\n`);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(destAbs), { recursive: true });
|
|
171
208
|
let transport = "git mv";
|
|
172
209
|
try {
|
|
173
210
|
(0, node_child_process_1.execFileSync)("git", ["mv", "--", sourcePath, destPath], {
|
|
@@ -194,8 +231,11 @@ function migrateSmartDocs(plan, repoRoot = (0, node_path_1.resolve)(process.cwd(
|
|
|
194
231
|
seenRecords.add(recordKey(record));
|
|
195
232
|
}
|
|
196
233
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
234
|
+
scaffoldBundleRoot(repoRoot, dryRun);
|
|
235
|
+
if (!dryRun) {
|
|
236
|
+
savePlan(repoRoot, effectivePlan);
|
|
237
|
+
saveProvenance(repoRoot, provenanceRecords);
|
|
238
|
+
Object.assign(plan, effectivePlan);
|
|
239
|
+
}
|
|
200
240
|
return Promise.resolve();
|
|
201
241
|
}
|
|
@@ -90,6 +90,7 @@ function generateLibrarianPacket(options) {
|
|
|
90
90
|
// Prohibited: anything that is not documentation or cognition
|
|
91
91
|
const prohibitedWritePaths = [
|
|
92
92
|
stateFile,
|
|
93
|
+
node_path_1.default.join(repoRoot, ".polaris", "clusters", clusterId, "cluster-state.json"),
|
|
93
94
|
node_path_1.default.join(repoRoot, ".polaris", "clusters", clusterId, "state.json"),
|
|
94
95
|
node_path_1.default.join(repoRoot, ".taskchain_artifacts"),
|
|
95
96
|
node_path_1.default.join(repoRoot, ".polaris", "runs"),
|
|
@@ -143,16 +144,24 @@ function generateLibrarianPacket(options) {
|
|
|
143
144
|
}
|
|
144
145
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
145
146
|
function resolveStateFile(repoRoot, clusterId) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
// Note: .polaris/clusters/<id>/cluster-state.json is intentionally not a candidate here.
|
|
148
|
+
// It holds the ClusterState schema (child_states, packet_pointers, ...), not the LoopState
|
|
149
|
+
// schema (completed_children, run_id, open_children_meta, ...) that readState()/this
|
|
150
|
+
// function require.
|
|
151
|
+
// Check per-cluster run-state convention: state.json
|
|
152
|
+
const legacyState = node_path_1.default.join(repoRoot, ".polaris", "clusters", clusterId, "state.json");
|
|
153
|
+
if (node_fs_1.default.existsSync(legacyState))
|
|
154
|
+
return legacyState;
|
|
155
|
+
// Check taskchain artifacts fallback
|
|
149
156
|
const taskchain = node_path_1.default.join(repoRoot, ".taskchain_artifacts", "polaris-run", "current-state.json");
|
|
150
157
|
if (node_fs_1.default.existsSync(taskchain))
|
|
151
158
|
return taskchain;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
159
|
+
// Check legacy runs fallback
|
|
160
|
+
const legacyRuns = node_path_1.default.join(repoRoot, ".polaris", "runs", "current-state.json");
|
|
161
|
+
if (node_fs_1.default.existsSync(legacyRuns))
|
|
162
|
+
return legacyRuns;
|
|
163
|
+
// Return per-cluster state path as default
|
|
164
|
+
return legacyState;
|
|
156
165
|
}
|
|
157
166
|
function findResultFile(resultsDir, childId) {
|
|
158
167
|
try {
|
|
@@ -65,6 +65,7 @@ function writeSealedResultFromSummary(packet, parsedSummary) {
|
|
|
65
65
|
const validation = parsedSummary["validation"] ?? parsedSummary["validation_summary"];
|
|
66
66
|
const sealedResult = {
|
|
67
67
|
run_id: packet.run_id,
|
|
68
|
+
cluster_id: packet.cluster_id,
|
|
68
69
|
child_id: packet.active_child,
|
|
69
70
|
status: normalizeSealedStatus(parsedSummary["status"]),
|
|
70
71
|
};
|
|
@@ -320,6 +320,7 @@ class TerminalCliAdapter {
|
|
|
320
320
|
const sealedResult = {
|
|
321
321
|
...normalized,
|
|
322
322
|
run_id: packet.run_id,
|
|
323
|
+
cluster_id: packet.cluster_id,
|
|
323
324
|
child_id: String(normalized["child_id"] ?? packet.active_child),
|
|
324
325
|
status: effectiveStatus,
|
|
325
326
|
commit: typeof normalized["commit"] === "string"
|
package/dist/loop/body-parser.js
CHANGED
|
@@ -123,6 +123,10 @@ function parseSections(body) {
|
|
|
123
123
|
/**
|
|
124
124
|
* Extracts bullet list items from the given text and returns them as trimmed strings.
|
|
125
125
|
*
|
|
126
|
+
* This is a generic list parser used by all section types (Validation, Acceptance
|
|
127
|
+
* Criteria, Ordering, Non-goals, etc.). It does NOT strip parenthetical annotations
|
|
128
|
+
* — that logic is scope-specific and handled separately.
|
|
129
|
+
*
|
|
126
130
|
* @param text - Section content to scan for bullet list lines (lines starting with `- ` or `* `)
|
|
127
131
|
* @returns The extracted list item strings, trimmed of whitespace; empty items are omitted.
|
|
128
132
|
*/
|
|
@@ -133,9 +137,25 @@ function parseListItems(text) {
|
|
|
133
137
|
.map((line) => line.replace(/^\s*[-*]\s+/, '').trim())
|
|
134
138
|
.filter((s) => s.length > 0);
|
|
135
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Removes trailing parenthetical annotations from a string.
|
|
142
|
+
*
|
|
143
|
+
* Used exclusively for Scope section items to strip annotations like "(new)" or
|
|
144
|
+
* "(thread flag through...)" so that allowed_scope entries in worker packets
|
|
145
|
+
* contain only bare file paths or valid glob patterns.
|
|
146
|
+
*
|
|
147
|
+
* @param s - The string to process
|
|
148
|
+
* @returns The input string with trailing `(...)` removed, re-trimmed
|
|
149
|
+
*/
|
|
150
|
+
function stripTrailingParenthetical(s) {
|
|
151
|
+
return s.replace(/\s*\([^)]*\)\s*$/, '').trim();
|
|
152
|
+
}
|
|
136
153
|
/**
|
|
137
154
|
* Extracts list items from the first section whose header matches a provided set.
|
|
138
155
|
*
|
|
156
|
+
* This is a generic section parser that does NOT apply any parenthetical stripping.
|
|
157
|
+
* For Scope sections, use `findScopeSection` instead.
|
|
158
|
+
*
|
|
139
159
|
* @param sections - Map of normalized header names to their section content; iteration follows the map's order
|
|
140
160
|
* @param headers - Set of normalized header names to match against section headers
|
|
141
161
|
* @returns An array of parsed list-item strings from the first matching section, or an empty array if no match is found
|
|
@@ -148,6 +168,25 @@ function findSection(sections, headers) {
|
|
|
148
168
|
}
|
|
149
169
|
return [];
|
|
150
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Extracts list items from the Scope section, stripping trailing parenthetical annotations.
|
|
173
|
+
*
|
|
174
|
+
* Scope section items often include human-readable annotations like "(new)" or
|
|
175
|
+
* "(thread flag through...)". These are stripped so that allowed_scope entries in
|
|
176
|
+
* worker packets contain only bare file paths or valid glob patterns.
|
|
177
|
+
*
|
|
178
|
+
* @param sections - Map of normalized header names to their section content
|
|
179
|
+
* @param headers - Set of normalized Scope header variants (e.g., SCOPE_HEADERS)
|
|
180
|
+
* @returns An array of scope paths with parentheticals removed, or an empty array if no Scope section is found
|
|
181
|
+
*/
|
|
182
|
+
function findScopeSection(sections, headers) {
|
|
183
|
+
for (const [header, content] of sections) {
|
|
184
|
+
if (headers.has(header)) {
|
|
185
|
+
return parseListItems(content).map(stripTrailingParenthetical).filter((s) => s.length > 0);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
151
190
|
/**
|
|
152
191
|
* Returns trimmed prose text from the first section matching a header set.
|
|
153
192
|
* Used for single-paragraph sections (Objective, Context, Goal).
|
|
@@ -189,7 +228,7 @@ function parseIssueBody(body) {
|
|
|
189
228
|
};
|
|
190
229
|
}
|
|
191
230
|
const sections = parseSections(body);
|
|
192
|
-
const rawScope =
|
|
231
|
+
const rawScope = findScopeSection(sections, SCOPE_HEADERS);
|
|
193
232
|
const scopeBlocked = rawScope.length > 0 && rawScope.every((item) => TBD_BLOCKED_RE.test(item));
|
|
194
233
|
const filteredScope = rawScope.filter((item) => !TBD_BLOCKED_RE.test(item));
|
|
195
234
|
return {
|
package/dist/loop/continue.js
CHANGED
|
@@ -14,6 +14,8 @@ const canon_check_js_1 = require("../smartdocs-engine/canon-check.js");
|
|
|
14
14
|
const dispatch_boundary_js_1 = require("./dispatch-boundary.js");
|
|
15
15
|
const ledger_js_1 = require("./ledger.js");
|
|
16
16
|
const librarian_dispatch_js_1 = require("../cognition/librarian-dispatch.js");
|
|
17
|
+
const index_js_1 = require("../tracker/index.js");
|
|
18
|
+
const lifecycle_transition_js_1 = require("../tracker/lifecycle-transition.js");
|
|
17
19
|
/** Resolves the expected sealed result file path for a child from its dispatch metadata. */
|
|
18
20
|
function resolveResultFileForChild(state, childId) {
|
|
19
21
|
const meta = state.open_children_meta?.[childId];
|
|
@@ -206,6 +208,10 @@ function countLoopAbortedEvents(telemetryFile, runId) {
|
|
|
206
208
|
.filter((event) => event.event === "loop-aborted" && event.run_id === runId)
|
|
207
209
|
.length;
|
|
208
210
|
}
|
|
211
|
+
function appendTelemetryEvent(telemetryFile, event) {
|
|
212
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(telemetryFile), { recursive: true });
|
|
213
|
+
(0, node_fs_1.appendFileSync)(telemetryFile, JSON.stringify(event) + "\n", "utf-8");
|
|
214
|
+
}
|
|
209
215
|
function resolveTelemetryFilePath(state, repoRoot) {
|
|
210
216
|
const artifactDir = state.artifact_dir ?? (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run");
|
|
211
217
|
return (0, node_path_1.join)(artifactDir, "runs", state.run_id, "telemetry.jsonl");
|
|
@@ -430,6 +436,71 @@ function runLoopContinue(options) {
|
|
|
430
436
|
process.exit(1);
|
|
431
437
|
}
|
|
432
438
|
}
|
|
439
|
+
// ── Apply lifecycle transition for child-validation-passed event ───────
|
|
440
|
+
// Fire-and-forget: tracker mutations must not block state checkpointing.
|
|
441
|
+
// Policy default targets "in_review" (not "done") — "done" is reserved for
|
|
442
|
+
// the child-merged event, applied once the delivering PR actually merges.
|
|
443
|
+
let transitionAdapter;
|
|
444
|
+
let transitionConfig;
|
|
445
|
+
const transitionTelemetryFile = resolveTelemetryFilePath(state, repoRoot);
|
|
446
|
+
try {
|
|
447
|
+
transitionConfig = (0, loader_js_1.loadConfig)(repoRoot);
|
|
448
|
+
transitionAdapter = (0, index_js_1.loadTrackerAdapter)(transitionConfig);
|
|
449
|
+
}
|
|
450
|
+
catch (err) {
|
|
451
|
+
appendTelemetryEvent(transitionTelemetryFile, {
|
|
452
|
+
event: "lifecycle-transition-error",
|
|
453
|
+
run_id: state.run_id,
|
|
454
|
+
child_id: completedChild,
|
|
455
|
+
transition_event: "child-validation-passed",
|
|
456
|
+
error: `Failed to load config or tracker adapter: ${err instanceof Error ? err.message : String(err)}`,
|
|
457
|
+
timestamp: new Date().toISOString(),
|
|
458
|
+
});
|
|
459
|
+
transitionAdapter = null;
|
|
460
|
+
transitionConfig = null;
|
|
461
|
+
}
|
|
462
|
+
// Note: transitionAdapter may legitimately be null (no tracker adapter configured) —
|
|
463
|
+
// that's not an error, and applyTransitionSafe/applyTransition handle a null adapter
|
|
464
|
+
// by returning a skip result. Only skip the attempt entirely when config loading itself
|
|
465
|
+
// failed (transitionConfig is null).
|
|
466
|
+
if (transitionConfig) {
|
|
467
|
+
new lifecycle_transition_js_1.LifecycleTransitionService()
|
|
468
|
+
.applyTransitionSafe({
|
|
469
|
+
adapter: transitionAdapter,
|
|
470
|
+
policy: transitionConfig.tracker?.lifecyclePolicy,
|
|
471
|
+
taskId: completedChild,
|
|
472
|
+
event: "child-validation-passed",
|
|
473
|
+
evidence: {
|
|
474
|
+
commit: completionCommit,
|
|
475
|
+
validationResults: completionValidation,
|
|
476
|
+
},
|
|
477
|
+
timestamp: new Date().toISOString(),
|
|
478
|
+
})
|
|
479
|
+
.then((result) => {
|
|
480
|
+
appendTelemetryEvent(transitionTelemetryFile, {
|
|
481
|
+
event: "lifecycle-transition-attempt",
|
|
482
|
+
run_id: state.run_id,
|
|
483
|
+
child_id: completedChild,
|
|
484
|
+
transition_event: result.event,
|
|
485
|
+
target_state: result.targetState,
|
|
486
|
+
applied: result.applied,
|
|
487
|
+
skipped: result.skipped,
|
|
488
|
+
skip_reason: result.skipReason,
|
|
489
|
+
error: result.error,
|
|
490
|
+
timestamp: result.timestamp,
|
|
491
|
+
});
|
|
492
|
+
})
|
|
493
|
+
.catch((err) => {
|
|
494
|
+
appendTelemetryEvent(transitionTelemetryFile, {
|
|
495
|
+
event: "lifecycle-transition-error",
|
|
496
|
+
run_id: state.run_id,
|
|
497
|
+
child_id: completedChild,
|
|
498
|
+
transition_event: "child-validation-passed",
|
|
499
|
+
error: err instanceof Error ? err.message : String(err),
|
|
500
|
+
timestamp: new Date().toISOString(),
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
}
|
|
433
504
|
}
|
|
434
505
|
if (completedChild && completionResultFile) {
|
|
435
506
|
const telemetryFile = resolveTelemetryFilePath(state, repoRoot);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.runCanonCheck = runCanonCheck;
|
|
4
|
+
exports.runSmartDocsLinkCheck = runSmartDocsLinkCheck;
|
|
4
5
|
const node_fs_1 = require("node:fs");
|
|
5
6
|
const node_path_1 = require("node:path");
|
|
7
|
+
const doctrine_js_1 = require("./doctrine.js");
|
|
6
8
|
// Modal verbs that indicate behavioral assertions in doctrine/spec files
|
|
7
9
|
const MODAL_VERBS = /\b(must|never|always|should|required|requires|shall)\b/i;
|
|
8
10
|
// Domains matched by filename keywords
|
|
@@ -338,3 +340,81 @@ function writeCandidateDraftDocs(conflicts, repoRoot, childId) {
|
|
|
338
340
|
// Non-fatal
|
|
339
341
|
}
|
|
340
342
|
}
|
|
343
|
+
/** Subdirectories of smartdocs/ that are subject to strict link checking */
|
|
344
|
+
const STRICT_SMARTDOCS_DIRS = [
|
|
345
|
+
(0, node_path_1.join)("smartdocs", "doctrine", "candidate"),
|
|
346
|
+
(0, node_path_1.join)("smartdocs", "doctrine", "active"),
|
|
347
|
+
(0, node_path_1.join)("smartdocs", "specs", "candidate"),
|
|
348
|
+
(0, node_path_1.join)("smartdocs", "specs", "active"),
|
|
349
|
+
];
|
|
350
|
+
/**
|
|
351
|
+
* Recursively walk a directory and return all .md file paths.
|
|
352
|
+
*
|
|
353
|
+
* @param dir - Directory to walk
|
|
354
|
+
* @returns Array of absolute paths to all .md files found recursively
|
|
355
|
+
*/
|
|
356
|
+
function walkMarkdownFiles(dir) {
|
|
357
|
+
const results = [];
|
|
358
|
+
let entries;
|
|
359
|
+
try {
|
|
360
|
+
entries = (0, node_fs_1.readdirSync)(dir, { withFileTypes: true });
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return results;
|
|
364
|
+
}
|
|
365
|
+
for (const entry of entries) {
|
|
366
|
+
const fullPath = (0, node_path_1.join)(dir, entry.name);
|
|
367
|
+
if (entry.isDirectory()) {
|
|
368
|
+
results.push(...walkMarkdownFiles(fullPath));
|
|
369
|
+
}
|
|
370
|
+
else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
371
|
+
results.push(fullPath);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return results;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Walk the strict-tier smartdocs directories (candidate/ and active/ under doctrine/ and specs/)
|
|
378
|
+
* and check every .md file for broken cross-links into smartdocs/.
|
|
379
|
+
*
|
|
380
|
+
* Recursively traverses subdirectories to find nested documents.
|
|
381
|
+
* Files in raw/ are never checked (OKF §5.3 permissive default).
|
|
382
|
+
* Broken links are reported as SpecConflict entries with type "stale-assumption".
|
|
383
|
+
*
|
|
384
|
+
* @param options - Options including repoRoot, runId, and telemetryFile
|
|
385
|
+
* @returns Object with all detected conflicts and the count of files checked
|
|
386
|
+
*/
|
|
387
|
+
function runSmartDocsLinkCheck(options) {
|
|
388
|
+
const { repoRoot, runId, telemetryFile, childId } = options;
|
|
389
|
+
const root = (0, node_path_1.resolve)(repoRoot);
|
|
390
|
+
const allConflicts = [];
|
|
391
|
+
let filesChecked = 0;
|
|
392
|
+
for (const subdir of STRICT_SMARTDOCS_DIRS) {
|
|
393
|
+
const dir = (0, node_path_1.join)(root, subdir);
|
|
394
|
+
if (!(0, node_fs_1.existsSync)(dir))
|
|
395
|
+
continue;
|
|
396
|
+
const markdownFiles = walkMarkdownFiles(dir);
|
|
397
|
+
for (const filePath of markdownFiles) {
|
|
398
|
+
let content;
|
|
399
|
+
try {
|
|
400
|
+
content = (0, node_fs_1.readFileSync)(filePath, "utf-8");
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
filesChecked++;
|
|
406
|
+
const conflicts = (0, doctrine_js_1.checkSmartDocsLinks)(filePath, content, root);
|
|
407
|
+
allConflicts.push(...conflicts);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
appendTelemetry(telemetryFile, {
|
|
411
|
+
event: "smartdocs-link-check-result",
|
|
412
|
+
run_id: runId,
|
|
413
|
+
child_id: childId ?? null,
|
|
414
|
+
files_checked: filesChecked,
|
|
415
|
+
broken_links: allConflicts.length,
|
|
416
|
+
conflicts: allConflicts,
|
|
417
|
+
timestamp: new Date().toISOString(),
|
|
418
|
+
});
|
|
419
|
+
return { conflicts: allConflicts, filesChecked };
|
|
420
|
+
}
|