pagegraph 0.11.0 → 0.12.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.
- package/README.md +140 -137
- package/dist/audit.d.ts +1 -1
- package/dist/audit.js +1 -1
- package/dist/build-only/audit.d.ts +1 -0
- package/dist/build-only/audit.js +6 -0
- package/dist/build-only/audit.js.map +1 -0
- package/dist/build-only/config.d.ts +1 -0
- package/dist/build-only/config.js +6 -0
- package/dist/build-only/config.js.map +1 -0
- package/dist/build-only/tanstack-start.d.ts +1 -0
- package/dist/build-only/tanstack-start.js +6 -0
- package/dist/build-only/tanstack-start.js.map +1 -0
- package/dist/build-only/vite.d.ts +1 -0
- package/dist/build-only/vite.js +6 -0
- package/dist/build-only/vite.js.map +1 -0
- package/dist/{checks-nk8ZRRIA.js → checks-uZk5Em9A.js} +3 -141
- package/dist/checks-uZk5Em9A.js.map +1 -0
- package/dist/cli.js +393 -287
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +7 -66
- package/dist/config.js +3 -122
- package/dist/config.js.map +1 -1
- package/dist/{content-DVL2iyGU.d.ts → content-B1cPtL8b.d.ts} +4 -128
- package/dist/{content-CB_kHmJ3.js → content-kcEnQdfi.js} +3 -2
- package/dist/{content-CB_kHmJ3.js.map → content-kcEnQdfi.js.map} +1 -1
- package/dist/declare-D5o7hwmv.js +9 -0
- package/dist/declare-D5o7hwmv.js.map +1 -0
- package/dist/declare-DVgdEdOL.d.ts +12127 -0
- package/dist/graph-Bcsourlv.js +162 -0
- package/dist/graph-Bcsourlv.js.map +1 -0
- package/dist/graph-BxCY90vq.d.ts +104 -0
- package/dist/graph-CBIY71w6.d.ts +57 -0
- package/dist/index.d.ts +37 -73
- package/dist/index.js +23 -134
- package/dist/index.js.map +1 -1
- package/dist/message-DT_CjUdt.js +12 -0
- package/dist/message-DT_CjUdt.js.map +1 -0
- package/dist/page-heads-6y-CARRQ.js +25 -0
- package/dist/page-heads-6y-CARRQ.js.map +1 -0
- package/dist/page-heads-DNkjMUEN.d.ts +21 -0
- package/dist/projections-D0oOFKiY.js +142 -0
- package/dist/projections-D0oOFKiY.js.map +1 -0
- package/dist/projections-DeEFe7XT.d.ts +78 -0
- package/dist/react.d.ts +26 -12051
- package/dist/react.js +12 -3
- package/dist/react.js.map +1 -1
- package/dist/route-config-35ZEzkNC.js +308 -0
- package/dist/route-config-35ZEzkNC.js.map +1 -0
- package/dist/route-config-wneg6ktn.d.ts +29 -0
- package/dist/tanstack-start/index.d.ts +40 -0
- package/dist/tanstack-start/index.js +368 -0
- package/dist/tanstack-start/index.js.map +1 -0
- package/dist/tanstack-start/server.d.ts +14 -0
- package/dist/tanstack-start/server.js +53 -0
- package/dist/tanstack-start/server.js.map +1 -0
- package/dist/vite-graph-loader-01p1WxEq.js +104 -0
- package/dist/vite-graph-loader-01p1WxEq.js.map +1 -0
- package/dist/vite-graph-loader-Btgjz5RH.d.ts +56 -0
- package/dist/vite.d.ts +2 -28
- package/dist/vite.js +1 -307
- package/dist/wire-B4RQMc4e.js +26 -0
- package/dist/wire-B4RQMc4e.js.map +1 -0
- package/package.json +27 -5
- package/dist/checks-nk8ZRRIA.js.map +0 -1
- package/dist/vite.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -35946,7 +35946,7 @@ const runWith = (command, config) => {
|
|
|
35946
35946
|
};
|
|
35947
35947
|
//#endregion
|
|
35948
35948
|
//#region package.json
|
|
35949
|
-
var version = "0.
|
|
35949
|
+
var version = "0.12.0";
|
|
35950
35950
|
//#endregion
|
|
35951
35951
|
//#region src/core/links.ts
|
|
35952
35952
|
const TOKEN = /<(\/?)(nav|footer|header)\b[^>]*>|<a\b([^>]*)>([\s\S]*?)<\/a>/gi;
|
|
@@ -38254,7 +38254,7 @@ const isWorkflowConfig = (value) => {
|
|
|
38254
38254
|
* an undefined `origin` would otherwise surface as "undefined/pricing" in a
|
|
38255
38255
|
* rendered sitemap rather than as an error here.
|
|
38256
38256
|
*/
|
|
38257
|
-
const isSeoCliConfig = (value) => isObject$1(value) && isFunction(value["loadGraph"]) && isString(value["origin"]) && isStringArray(value["disallow"]) && (value["contentSignal"] === void 0 || isString(value["contentSignal"])) && (value["directives"] === void 0 || isStringArray(value["directives"])) && (value["transform"] === void 0 || isFunction(value["transform"])) && (value["coverage"] === void 0 || isCoverageRules(value["coverage"])) && (value["content"] === void 0 || isContentPolicy(value["content"])) && (value["freshness"] === void 0 || isFreshnessPolicy(value["freshness"])) && (value["workflows"] === void 0 || isWorkflowConfig(value["workflows"]));
|
|
38257
|
+
const isSeoCliConfig = (value) => isObject$1(value) && isFunction(value["loadGraph"]) && isString(value["origin"]) && (value["disallow"] === void 0 || isStringArray(value["disallow"])) && (value["contentSignal"] === void 0 || isString(value["contentSignal"])) && (value["directives"] === void 0 || isStringArray(value["directives"])) && (value["transform"] === void 0 || isFunction(value["transform"])) && (value["coverage"] === void 0 || isCoverageRules(value["coverage"])) && (value["content"] === void 0 || isContentPolicy(value["content"])) && (value["freshness"] === void 0 || isFreshnessPolicy(value["freshness"])) && (value["workflows"] === void 0 || isWorkflowConfig(value["workflows"]));
|
|
38258
38258
|
/** Import and validate one config path. */
|
|
38259
38259
|
const loadConfigFile = (configPath) => gen(function* () {
|
|
38260
38260
|
yield* logDebug(`Loading SEO config from ${configPath}`);
|
|
@@ -38262,7 +38262,7 @@ const loadConfigFile = (configPath) => gen(function* () {
|
|
|
38262
38262
|
try: () => import(pathToFileURL(configPath).href),
|
|
38263
38263
|
catch: (cause) => new SeoCliError({ message: `Could not load ${configPath}: ${messageOf$2(cause)}` })
|
|
38264
38264
|
});
|
|
38265
|
-
if (!isSeoCliConfig(module.default)) return yield* new SeoCliError({ message: `${configPath} must default-export defineSeoConfig({ origin,
|
|
38265
|
+
if (!isSeoCliConfig(module.default)) return yield* new SeoCliError({ message: `${configPath} must default-export defineSeoConfig({ origin, loadGraph }).` });
|
|
38266
38266
|
return module.default;
|
|
38267
38267
|
});
|
|
38268
38268
|
/**
|
|
@@ -38272,7 +38272,7 @@ const loadConfigFile = (configPath) => gen(function* () {
|
|
|
38272
38272
|
const loadSeoConfig = gen(function* () {
|
|
38273
38273
|
const cwd = process.cwd();
|
|
38274
38274
|
const configPath = findConfigFile(cwd);
|
|
38275
|
-
if (configPath === void 0) return yield* new SeoCliError({ message: `No ${CONFIG_FILENAMES[0]} in ${cwd} or any parent directory. Create one that exports \`defineSeoConfig({ origin,
|
|
38275
|
+
if (configPath === void 0) return yield* new SeoCliError({ message: `No ${CONFIG_FILENAMES[0]} in ${cwd} or any parent directory. Create one that exports \`defineSeoConfig({ origin, loadGraph })\` from "pagegraph/config".` });
|
|
38276
38276
|
return yield* loadConfigFile(configPath);
|
|
38277
38277
|
});
|
|
38278
38278
|
/** Load the config together with the project root that owns it. */
|
|
@@ -38302,15 +38302,17 @@ const loadSeoConfigOptional = gen(function* () {
|
|
|
38302
38302
|
* {@link viteGraphLoader}, an in-process Vite server) is released when the
|
|
38303
38303
|
* surrounding `Effect.scoped` exits, on success or failure.
|
|
38304
38304
|
*/
|
|
38305
|
-
const
|
|
38305
|
+
const acquireLoadedGraph = (config) => gen(function* () {
|
|
38306
38306
|
yield* logDebug("Loading the SEO graph…");
|
|
38307
38307
|
const loaded = yield* acquireRelease(tryPromise({
|
|
38308
38308
|
try: () => config.loadGraph(),
|
|
38309
38309
|
catch: (cause) => new SeoCliError({ message: messageOf$2(cause) })
|
|
38310
38310
|
}), (acquired) => promise(() => acquired.dispose()).pipe(catchDefect((defect) => logWarning(`Could not dispose the SEO graph loader: ${messageOf$2(defect)}`))));
|
|
38311
38311
|
yield* logDebug(`Loaded SEO graph: ${loaded.graph.nodes.size} nodes, ${loaded.graph.edges.length} edges.`);
|
|
38312
|
-
return loaded
|
|
38312
|
+
return loaded;
|
|
38313
38313
|
});
|
|
38314
|
+
/** {@link acquireLoadedGraph}, for commands that need only the graph. */
|
|
38315
|
+
const acquireGraph = (config) => map$5(acquireLoadedGraph(config), (loaded) => loaded.graph);
|
|
38314
38316
|
//#endregion
|
|
38315
38317
|
//#region src/cli/render.ts
|
|
38316
38318
|
/** Count of edges pointing *at* each node — 0 means nothing links to it. */
|
|
@@ -40581,11 +40583,22 @@ const loadWorkflowOpenCode = async () => {
|
|
|
40581
40583
|
};
|
|
40582
40584
|
//#endregion
|
|
40583
40585
|
//#region src/workflows/opencode.ts
|
|
40584
|
-
var
|
|
40586
|
+
var WorkflowHostError = class extends Error {
|
|
40587
|
+
sessionId;
|
|
40588
|
+
transcript;
|
|
40589
|
+
constructor(message, sessionId, transcript, cause) {
|
|
40590
|
+
super(message, { cause });
|
|
40591
|
+
this.sessionId = sessionId;
|
|
40592
|
+
this.transcript = transcript;
|
|
40593
|
+
this.name = "WorkflowHostError";
|
|
40594
|
+
}
|
|
40595
|
+
};
|
|
40596
|
+
var MissingExecutorEvidenceError = class extends WorkflowHostError {
|
|
40585
40597
|
result;
|
|
40586
40598
|
constructor(result) {
|
|
40587
|
-
super(`The SEO agent returned without the required Executor evidence (searches: ${result.executor.searches.length}, calls: ${result.executor.calls.length})
|
|
40599
|
+
super(`The SEO agent returned without the required Executor evidence (searches: ${result.executor.searches.length}, calls: ${result.executor.calls.length}).`, result.sessionId, result.transcript, void 0);
|
|
40588
40600
|
this.result = result;
|
|
40601
|
+
this.name = "MissingExecutorEvidenceError";
|
|
40589
40602
|
}
|
|
40590
40603
|
};
|
|
40591
40604
|
const requireExecutorEvidence = async (result, recover) => {
|
|
@@ -40653,15 +40666,18 @@ const parseWorkflowState = (transcript) => {
|
|
|
40653
40666
|
return isSome(text) && text.value.text.includes("{") ? [text.value.text] : [];
|
|
40654
40667
|
});
|
|
40655
40668
|
});
|
|
40669
|
+
let parseCause;
|
|
40656
40670
|
for (const text of candidates.reverse()) {
|
|
40657
40671
|
const start = text.indexOf("{");
|
|
40658
40672
|
const end = text.lastIndexOf("}");
|
|
40659
40673
|
if (start === -1 || end <= start) continue;
|
|
40660
40674
|
try {
|
|
40661
40675
|
return JSON.parse(text.slice(start, end + 1));
|
|
40662
|
-
} catch {
|
|
40676
|
+
} catch (cause) {
|
|
40677
|
+
parseCause = cause;
|
|
40678
|
+
}
|
|
40663
40679
|
}
|
|
40664
|
-
throw new Error("The SEO agent did not return a valid workflow JSON object.");
|
|
40680
|
+
throw new Error("The SEO agent did not return a valid workflow JSON object.", { cause: parseCause });
|
|
40665
40681
|
};
|
|
40666
40682
|
const parseWorkflowStateWithRepair = async (transcript, repair) => {
|
|
40667
40683
|
try {
|
|
@@ -40669,12 +40685,16 @@ const parseWorkflowStateWithRepair = async (transcript, repair) => {
|
|
|
40669
40685
|
state: parseWorkflowState(transcript),
|
|
40670
40686
|
transcript
|
|
40671
40687
|
};
|
|
40672
|
-
} catch {
|
|
40673
|
-
|
|
40674
|
-
|
|
40675
|
-
|
|
40676
|
-
|
|
40677
|
-
|
|
40688
|
+
} catch (firstError) {
|
|
40689
|
+
try {
|
|
40690
|
+
const repaired = await repair();
|
|
40691
|
+
return {
|
|
40692
|
+
state: parseWorkflowState(repaired),
|
|
40693
|
+
transcript: repaired
|
|
40694
|
+
};
|
|
40695
|
+
} catch (repairError) {
|
|
40696
|
+
throw new AggregateError([firstError, repairError], "The SEO agent did not return valid workflow JSON after one repair.", { cause: repairError });
|
|
40697
|
+
}
|
|
40678
40698
|
}
|
|
40679
40699
|
};
|
|
40680
40700
|
const STATE_REPAIR_PROMPT = [
|
|
@@ -40925,70 +40945,93 @@ const acquireWorkflowHost = async (options) => {
|
|
|
40925
40945
|
const location = { directory: options.root };
|
|
40926
40946
|
if (!await waitForActiveExecutorPlugin({ list: async () => (await openCodeOperation("GET /api/plugin (plugin.list)", () => host.plugin.list({ location }))).data })) throw new Error(`No active Executor plugin was discovered in ${configDirectory}; agentic SEO workflows require Executor.`);
|
|
40927
40947
|
const cursors = /* @__PURE__ */ new Map();
|
|
40948
|
+
const transcripts = /* @__PURE__ */ new Map();
|
|
40928
40949
|
const complete = async (sessionId, prompt, workflowOptions) => {
|
|
40929
|
-
|
|
40930
|
-
|
|
40931
|
-
|
|
40932
|
-
|
|
40933
|
-
});
|
|
40934
|
-
const configuredTimeoutMs = options.timeoutMs ?? options.config.timeoutMs ?? 18e4;
|
|
40935
|
-
const deadline = Date.now() + configuredTimeoutMs;
|
|
40936
|
-
const remaining = () => Math.max(0, deadline - Date.now());
|
|
40937
|
-
const activity = {
|
|
40938
|
-
modelSteps: 0,
|
|
40939
|
-
completedTools: 0,
|
|
40940
|
-
toolNames: /* @__PURE__ */ new Map()
|
|
40941
|
-
};
|
|
40942
|
-
let cursor = await runWorkflowTurn(host, sessionId, prompt, {
|
|
40943
|
-
timeoutMs: remaining(),
|
|
40944
|
-
configuredTimeoutMs,
|
|
40945
|
-
stage: "prompt admission",
|
|
40946
|
-
after: cursors.get(sessionId),
|
|
40947
|
-
skills: workflowOptions.skills,
|
|
40948
|
-
activity
|
|
40949
|
-
});
|
|
40950
|
-
cursors.set(sessionId, cursor);
|
|
40951
|
-
let transcript = await host.sessions.export({
|
|
40952
|
-
sessionID: sessionId,
|
|
40953
|
-
sanitize: false
|
|
40954
|
-
});
|
|
40955
|
-
const parsed = await parseWorkflowStateWithRepair(transcript, async () => {
|
|
40956
|
-
const priorPermissions = [...workflowOptions.permissions ?? []];
|
|
40957
|
-
await host.sessions.update({
|
|
40950
|
+
let transcript = transcripts.get(sessionId) ?? { messages: [] };
|
|
40951
|
+
try {
|
|
40952
|
+
assertWorkflowSkillsAvailable(configDirectory, workflowOptions.skills);
|
|
40953
|
+
if (workflowOptions.permissions !== void 0) await host.sessions.update({
|
|
40958
40954
|
sessionID: sessionId,
|
|
40959
|
-
permissions: [
|
|
40960
|
-
action: "*",
|
|
40961
|
-
resource: "*",
|
|
40962
|
-
effect: "deny"
|
|
40963
|
-
}]
|
|
40955
|
+
permissions: [...workflowOptions.permissions]
|
|
40964
40956
|
});
|
|
40965
|
-
|
|
40966
|
-
|
|
40967
|
-
|
|
40968
|
-
|
|
40969
|
-
|
|
40970
|
-
|
|
40971
|
-
|
|
40957
|
+
const configuredTimeoutMs = options.timeoutMs ?? options.config.timeoutMs ?? 18e4;
|
|
40958
|
+
const deadline = Date.now() + configuredTimeoutMs;
|
|
40959
|
+
const remaining = () => Math.max(0, deadline - Date.now());
|
|
40960
|
+
const activity = {
|
|
40961
|
+
modelSteps: 0,
|
|
40962
|
+
completedTools: 0,
|
|
40963
|
+
toolNames: /* @__PURE__ */ new Map()
|
|
40964
|
+
};
|
|
40965
|
+
let cursor = await runWorkflowTurn(host, sessionId, prompt, {
|
|
40966
|
+
timeoutMs: remaining(),
|
|
40967
|
+
configuredTimeoutMs,
|
|
40968
|
+
stage: "prompt admission",
|
|
40969
|
+
after: cursors.get(sessionId),
|
|
40970
|
+
skills: workflowOptions.skills,
|
|
40971
|
+
activity
|
|
40972
|
+
});
|
|
40973
|
+
cursors.set(sessionId, cursor);
|
|
40974
|
+
transcript = await host.sessions.export({
|
|
40975
|
+
sessionID: sessionId,
|
|
40976
|
+
sanitize: false
|
|
40977
|
+
});
|
|
40978
|
+
transcripts.set(sessionId, transcript);
|
|
40979
|
+
const parsed = await parseWorkflowStateWithRepair(transcript, async () => {
|
|
40980
|
+
const priorPermissions = [...workflowOptions.permissions ?? []];
|
|
40981
|
+
await host.sessions.update({
|
|
40982
|
+
sessionID: sessionId,
|
|
40983
|
+
permissions: [{
|
|
40984
|
+
action: "*",
|
|
40985
|
+
resource: "*",
|
|
40986
|
+
effect: "deny"
|
|
40987
|
+
}]
|
|
40972
40988
|
});
|
|
40973
|
-
|
|
40989
|
+
let repairFailed = false;
|
|
40990
|
+
try {
|
|
40991
|
+
cursor = await runWorkflowTurn(host, sessionId, STATE_REPAIR_PROMPT, {
|
|
40992
|
+
timeoutMs: remaining(),
|
|
40993
|
+
configuredTimeoutMs,
|
|
40994
|
+
stage: "workflow state repair",
|
|
40995
|
+
after: cursor,
|
|
40996
|
+
activity
|
|
40997
|
+
});
|
|
40998
|
+
cursors.set(sessionId, cursor);
|
|
40999
|
+
transcript = await host.sessions.export({
|
|
41000
|
+
sessionID: sessionId,
|
|
41001
|
+
sanitize: false
|
|
41002
|
+
});
|
|
41003
|
+
transcripts.set(sessionId, transcript);
|
|
41004
|
+
return transcript;
|
|
41005
|
+
} catch (cause) {
|
|
41006
|
+
repairFailed = true;
|
|
41007
|
+
throw cause;
|
|
41008
|
+
} finally {
|
|
41009
|
+
try {
|
|
41010
|
+
await host.sessions.update({
|
|
41011
|
+
sessionID: sessionId,
|
|
41012
|
+
permissions: priorPermissions
|
|
41013
|
+
});
|
|
41014
|
+
} catch (cause) {
|
|
41015
|
+
if (!repairFailed) throw cause;
|
|
41016
|
+
}
|
|
41017
|
+
}
|
|
41018
|
+
});
|
|
41019
|
+
return {
|
|
41020
|
+
state: parsed.state,
|
|
41021
|
+
sessionId,
|
|
41022
|
+
transcript: parsed.transcript,
|
|
41023
|
+
executor: collectExecutorEvidence(parsed.transcript)
|
|
41024
|
+
};
|
|
41025
|
+
} catch (cause) {
|
|
41026
|
+
try {
|
|
40974
41027
|
transcript = await host.sessions.export({
|
|
40975
41028
|
sessionID: sessionId,
|
|
40976
41029
|
sanitize: false
|
|
40977
|
-
});
|
|
40978
|
-
|
|
40979
|
-
|
|
40980
|
-
|
|
40981
|
-
|
|
40982
|
-
permissions: priorPermissions
|
|
40983
|
-
});
|
|
40984
|
-
}
|
|
40985
|
-
});
|
|
40986
|
-
return {
|
|
40987
|
-
state: parsed.state,
|
|
40988
|
-
sessionId,
|
|
40989
|
-
transcript: parsed.transcript,
|
|
40990
|
-
executor: collectExecutorEvidence(parsed.transcript)
|
|
40991
|
-
};
|
|
41030
|
+
}, { signal: AbortSignal.timeout(5e3) });
|
|
41031
|
+
} catch {}
|
|
41032
|
+
transcripts.set(sessionId, transcript);
|
|
41033
|
+
throw new WorkflowHostError(openCodeErrorMessage(cause), sessionId, transcript, cause);
|
|
41034
|
+
}
|
|
40992
41035
|
};
|
|
40993
41036
|
return {
|
|
40994
41037
|
model,
|
|
@@ -41010,7 +41053,9 @@ const acquireWorkflowHost = async (options) => {
|
|
|
41010
41053
|
close: () => host.close()
|
|
41011
41054
|
};
|
|
41012
41055
|
} catch (cause) {
|
|
41013
|
-
|
|
41056
|
+
try {
|
|
41057
|
+
await host.close();
|
|
41058
|
+
} catch {}
|
|
41014
41059
|
throw cause;
|
|
41015
41060
|
}
|
|
41016
41061
|
};
|
|
@@ -44817,13 +44862,33 @@ const writeResearchCheckpoint = (root, runsDirectory, checkpoint) => {
|
|
|
44817
44862
|
writeFileSync(path, `${JSON.stringify(checkpoint, null, 2)}\n`, "utf8");
|
|
44818
44863
|
return path;
|
|
44819
44864
|
};
|
|
44820
|
-
const
|
|
44865
|
+
const writeWorkflowFailure = (root, runsDirectory, id, details) => {
|
|
44821
44866
|
const directory = resolve(root, runsDirectory, id);
|
|
44822
44867
|
mkdirSync(directory, { recursive: true });
|
|
44823
|
-
const path = resolve(directory, "
|
|
44868
|
+
const path = resolve(directory, "failure.json");
|
|
44824
44869
|
writeFileSync(path, `${JSON.stringify(details, null, 2)}\n`, "utf8");
|
|
44825
44870
|
return path;
|
|
44826
44871
|
};
|
|
44872
|
+
const serializeCause = (value, top = true, seen = /* @__PURE__ */ new Set()) => {
|
|
44873
|
+
if (seen.has(value)) return {
|
|
44874
|
+
name: "CircularCause",
|
|
44875
|
+
message: "Circular error cause"
|
|
44876
|
+
};
|
|
44877
|
+
if (value !== null && typeof value === "object") {
|
|
44878
|
+
seen.add(value);
|
|
44879
|
+
return {
|
|
44880
|
+
message: value instanceof Error ? value.message : "message" in value ? String(value.message) : JSON.stringify(value),
|
|
44881
|
+
name: value instanceof Error ? value.name : "name" in value ? String(value.name) : "Error",
|
|
44882
|
+
...top && value instanceof Error ? { stack: value.stack } : {},
|
|
44883
|
+
..."cause" in value ? { cause: serializeCause(value.cause, false, seen) } : {},
|
|
44884
|
+
...value instanceof AggregateError ? { errors: value.errors.map((error) => serializeCause(error, false, new Set(seen))) } : {}
|
|
44885
|
+
};
|
|
44886
|
+
}
|
|
44887
|
+
return {
|
|
44888
|
+
name: "Error",
|
|
44889
|
+
message: String(value)
|
|
44890
|
+
};
|
|
44891
|
+
};
|
|
44827
44892
|
//#endregion
|
|
44828
44893
|
//#region node_modules/effect/dist/ai/Decision.js
|
|
44829
44894
|
/**
|
|
@@ -45151,8 +45216,8 @@ const runDecisions = (options) => gen(function* () {
|
|
|
45151
45216
|
//#endregion
|
|
45152
45217
|
//#region src/workflows/specs/types.ts
|
|
45153
45218
|
const defineWorkflow = (definition) => {
|
|
45154
|
-
const decodeState = decodeUnknownSync(definition.stateSchema);
|
|
45155
|
-
const decodeInput = decodeUnknownSync(definition.family.input);
|
|
45219
|
+
const decodeState = decodeUnknownSync(definition.stateSchema, { errors: "all" });
|
|
45220
|
+
const decodeInput = decodeUnknownSync(definition.family.input, { errors: "all" });
|
|
45156
45221
|
const inputsFrom = (state) => definition.decisionInputs(decodeState(state));
|
|
45157
45222
|
const decodedInputs = (inputs) => inputs.map((input) => decodeInput(input));
|
|
45158
45223
|
const decodeDecisionMetadata = decodeUnknownSync(Struct({
|
|
@@ -46243,6 +46308,11 @@ const defaultDecide = async (inputs, spec) => {
|
|
|
46243
46308
|
});
|
|
46244
46309
|
}).pipe(provide(layer$14)));
|
|
46245
46310
|
};
|
|
46311
|
+
const dropNulls = (value) => {
|
|
46312
|
+
if (Array.isArray(value)) return value.filter((item) => item !== null).map(dropNulls);
|
|
46313
|
+
if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== null).map(([key, item]) => [key, dropNulls(item)]));
|
|
46314
|
+
return value;
|
|
46315
|
+
};
|
|
46246
46316
|
const capState = (state, limit) => {
|
|
46247
46317
|
if (state === null || typeof state !== "object" || Array.isArray(state)) return state;
|
|
46248
46318
|
const record = state;
|
|
@@ -46291,236 +46361,270 @@ const runWorkflow = async (input, dependencies = {}) => {
|
|
|
46291
46361
|
const now = dependencies.now ?? (() => /* @__PURE__ */ new Date());
|
|
46292
46362
|
const started = now();
|
|
46293
46363
|
const id = createRunId(started, spec.id);
|
|
46294
|
-
const
|
|
46295
|
-
|
|
46296
|
-
|
|
46297
|
-
|
|
46298
|
-
|
|
46299
|
-
|
|
46300
|
-
|
|
46301
|
-
|
|
46302
|
-
const evidence = collectWorkflowEvidence(input.graph, input.options, input.root, spec.id, workflows.context);
|
|
46303
|
-
const suggestionReport = input.options.suggestions === void 0 ? void 0 : (() => {
|
|
46304
|
-
if (spec.id !== "improve.links") throw new Error("--suggestions is only valid for improve links");
|
|
46305
|
-
const report = decodeLinksSuggestionReport(JSON.parse(readFileSync(resolve(input.root, input.options.suggestions), "utf8")), new URL(input.config.origin).origin);
|
|
46306
|
-
const selectedSources = selectGraph(input.graph, input.options).nodes;
|
|
46307
|
-
for (const candidate of report.candidates) {
|
|
46308
|
-
if (!input.graph.nodes.has(candidate.source) || !input.graph.nodes.has(candidate.destination)) throw new Error(`Suggestion references a path absent from seo.config.ts: ${candidate.source} → ${candidate.destination}`);
|
|
46309
|
-
if (!selectedSources.has(candidate.source)) throw new Error(`Suggestion source is outside workflow page/kind/limit targets: ${candidate.source}`);
|
|
46310
|
-
}
|
|
46311
|
-
return report;
|
|
46312
|
-
})();
|
|
46313
|
-
const suppliedEvidence = suggestionReport === void 0 ? evidence : {
|
|
46314
|
-
...evidence,
|
|
46315
|
-
suggestions: suggestionReport
|
|
46316
|
-
};
|
|
46317
|
-
const host = await (dependencies.acquireHost ?? acquireWorkflowHost)({
|
|
46318
|
-
root: input.root,
|
|
46319
|
-
config: workflows.opencode,
|
|
46320
|
-
model: input.model ?? workflows.opencode.models?.[spec.id]
|
|
46321
|
-
});
|
|
46364
|
+
const runsDirectory = input.out ?? workflows.runsDirectory ?? ".pagegraph/runs";
|
|
46365
|
+
let stage = "acquire";
|
|
46366
|
+
let sessionId;
|
|
46367
|
+
let transcript = null;
|
|
46368
|
+
const retainResult = (result) => {
|
|
46369
|
+
sessionId = result.sessionId;
|
|
46370
|
+
transcript = result.transcript;
|
|
46371
|
+
};
|
|
46322
46372
|
try {
|
|
46323
|
-
const
|
|
46324
|
-
const
|
|
46325
|
-
|
|
46373
|
+
const gitAtStart = inspectGit(input.root);
|
|
46374
|
+
const mutation = createWorkflowMutationPolicy({
|
|
46375
|
+
root: input.root,
|
|
46376
|
+
dirtyAtStart: gitAtStart.dirty,
|
|
46377
|
+
dryRun: spec.mutatesFiles ? input.options.dryRun : true,
|
|
46378
|
+
allowDirty: input.options.allowDirty
|
|
46379
|
+
});
|
|
46380
|
+
if (spec.mutatesFiles) mutation.assertStartAllowed();
|
|
46381
|
+
const evidence = collectWorkflowEvidence(input.graph, input.options, input.root, spec.id, workflows.context);
|
|
46382
|
+
const suggestionReport = input.options.suggestions === void 0 ? void 0 : (() => {
|
|
46383
|
+
if (spec.id !== "improve.links") throw new Error("--suggestions is only valid for improve links");
|
|
46384
|
+
const report = decodeLinksSuggestionReport(JSON.parse(readFileSync(resolve(input.root, input.options.suggestions), "utf8")), new URL(input.config.origin).origin);
|
|
46385
|
+
const selectedSources = selectGraph(input.graph, input.options).nodes;
|
|
46386
|
+
for (const candidate of report.candidates) {
|
|
46387
|
+
if (!input.graph.nodes.has(candidate.source) || !input.graph.nodes.has(candidate.destination)) throw new Error(`Suggestion references a path absent from seo.config.ts: ${candidate.source} → ${candidate.destination}`);
|
|
46388
|
+
if (!selectedSources.has(candidate.source)) throw new Error(`Suggestion source is outside workflow page/kind/limit targets: ${candidate.source}`);
|
|
46389
|
+
}
|
|
46390
|
+
return report;
|
|
46391
|
+
})();
|
|
46392
|
+
const suppliedEvidence = suggestionReport === void 0 ? evidence : {
|
|
46393
|
+
...evidence,
|
|
46394
|
+
suggestions: suggestionReport
|
|
46395
|
+
};
|
|
46396
|
+
const host = await (dependencies.acquireHost ?? acquireWorkflowHost)({
|
|
46397
|
+
root: input.root,
|
|
46398
|
+
config: workflows.opencode,
|
|
46399
|
+
model: input.model ?? workflows.opencode.models?.[spec.id]
|
|
46400
|
+
});
|
|
46401
|
+
let turnFailed = false;
|
|
46326
46402
|
try {
|
|
46327
|
-
|
|
46403
|
+
const researchPermissions = repositoryMutationPermissionRules(input.root);
|
|
46404
|
+
const actionPermissions = spec.mutatesFiles && !input.options.dryRun ? mutation.sessionPermissions : researchPermissions;
|
|
46405
|
+
stage = "research";
|
|
46406
|
+
let researched = await host.research(researchPrompt(spec, suppliedEvidence, input.options), {
|
|
46328
46407
|
skills: spec.skills,
|
|
46329
46408
|
permissions: researchPermissions
|
|
46330
46409
|
});
|
|
46331
|
-
|
|
46332
|
-
|
|
46333
|
-
const
|
|
46334
|
-
|
|
46335
|
-
|
|
46336
|
-
|
|
46337
|
-
|
|
46338
|
-
|
|
46339
|
-
|
|
46340
|
-
|
|
46341
|
-
|
|
46342
|
-
|
|
46343
|
-
|
|
46344
|
-
|
|
46345
|
-
|
|
46346
|
-
|
|
46347
|
-
|
|
46348
|
-
|
|
46349
|
-
|
|
46350
|
-
if (error !== void 0) throw new Error(error);
|
|
46351
|
-
}
|
|
46352
|
-
return decoded;
|
|
46353
|
-
};
|
|
46354
|
-
let state;
|
|
46355
|
-
try {
|
|
46356
|
-
state = decodeResearchState(researched.state);
|
|
46357
|
-
} catch (firstError) {
|
|
46358
|
-
const original = researched;
|
|
46359
|
-
let repaired;
|
|
46410
|
+
retainResult(researched);
|
|
46411
|
+
const assertResearchReadOnly = () => {
|
|
46412
|
+
const researchFiles = changedFiles(gitAtStart, inspectGit(input.root));
|
|
46413
|
+
if (researchFiles.length > 0) throw new Error(`${spec.id} changed repository files during its read-only research turn: ${researchFiles.join(", ")}`);
|
|
46414
|
+
};
|
|
46415
|
+
assertResearchReadOnly();
|
|
46416
|
+
const decodeResearchState = (value) => {
|
|
46417
|
+
const decoded = spec.decodeState(capState(dropNulls(value), input.options.limit));
|
|
46418
|
+
const issues = [];
|
|
46419
|
+
for (const [index, item] of spec.decisionInputs(decoded).entries()) try {
|
|
46420
|
+
const error = spec.validateDecisionInput(item);
|
|
46421
|
+
if (error !== void 0) issues.push(`decisionInputs[${index}]: ${error}`);
|
|
46422
|
+
} catch (cause) {
|
|
46423
|
+
issues.push(`decisionInputs[${index}]: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
46424
|
+
}
|
|
46425
|
+
if (issues.length > 0) throw new Error(issues.join("\n"));
|
|
46426
|
+
return decoded;
|
|
46427
|
+
};
|
|
46428
|
+
let state;
|
|
46360
46429
|
try {
|
|
46361
|
-
|
|
46362
|
-
|
|
46363
|
-
|
|
46364
|
-
|
|
46365
|
-
|
|
46366
|
-
|
|
46367
|
-
|
|
46368
|
-
|
|
46369
|
-
|
|
46370
|
-
|
|
46371
|
-
|
|
46372
|
-
|
|
46373
|
-
|
|
46374
|
-
|
|
46375
|
-
|
|
46376
|
-
|
|
46377
|
-
|
|
46378
|
-
|
|
46379
|
-
firstError: String(firstError),
|
|
46380
|
-
repairError: String(repairError)
|
|
46381
|
-
});
|
|
46382
|
-
throw new Error(`${spec.id} research state failed schema validation after one read-only repair. Research failure artifact: ${path}`, { cause: repairError });
|
|
46383
|
-
}
|
|
46384
|
-
}
|
|
46385
|
-
const decisionInputs = spec.decisionInputs(state);
|
|
46386
|
-
if (suggestionReport !== void 0) {
|
|
46387
|
-
const valid = new Set(suggestionReport.candidates.map((item) => `${item.source}\u0000${item.destination}\u0000${item.anchor}\u0000${item.sentence}`));
|
|
46388
|
-
for (const item of decisionInputs) {
|
|
46389
|
-
const link = item;
|
|
46390
|
-
if (!valid.has(`${link.from}\u0000${link.to}\u0000${link.anchor}\u0000${link.context}`)) throw new Error("Research proposed a link absent from the supplied suggestions");
|
|
46391
|
-
}
|
|
46392
|
-
}
|
|
46393
|
-
for (const item of decisionInputs) {
|
|
46394
|
-
const error = spec.validateDecisionInput(item);
|
|
46395
|
-
if (error !== void 0) throw new Error(error);
|
|
46396
|
-
}
|
|
46397
|
-
const runsDirectory = input.out ?? workflows.runsDirectory ?? ".pagegraph/runs";
|
|
46398
|
-
const checkpoint = {
|
|
46399
|
-
kind: "pagegraph-workflow-research-checkpoint",
|
|
46400
|
-
schemaVersion: 1,
|
|
46401
|
-
id,
|
|
46402
|
-
workflow: spec.id,
|
|
46403
|
-
startedAt: started.toISOString(),
|
|
46404
|
-
checkpointedAt: now().toISOString(),
|
|
46405
|
-
project: {
|
|
46406
|
-
root: input.root,
|
|
46407
|
-
head: gitAtStart.head,
|
|
46408
|
-
dirtyAtStart: gitAtStart.dirty,
|
|
46409
|
-
filesAtStart: gitAtStart.files
|
|
46410
|
-
},
|
|
46411
|
-
options: input.options,
|
|
46412
|
-
evidence: {
|
|
46413
|
-
...suppliedEvidence,
|
|
46414
|
-
executor: researched.executor
|
|
46415
|
-
},
|
|
46416
|
-
state,
|
|
46417
|
-
decisionInputs,
|
|
46418
|
-
opencode: {
|
|
46419
|
-
agent: "seo",
|
|
46420
|
-
sessionId: researched.sessionId,
|
|
46421
|
-
model: host.model,
|
|
46422
|
-
transcript: researched.transcript
|
|
46423
|
-
}
|
|
46424
|
-
};
|
|
46425
|
-
const checkpointPath = writeResearchCheckpoint(input.root, runsDirectory, checkpoint);
|
|
46426
|
-
const gitAtCheckpoint = inspectGit(input.root);
|
|
46427
|
-
try {
|
|
46428
|
-
const decisionReport = await (dependencies.decide ?? defaultDecide)(decisionInputs, spec);
|
|
46429
|
-
const accepted = spec.id === "improve.links" ? decisionReport.resolved.filter((record) => record.verdict === "add" || record.verdict === "update") : void 0;
|
|
46430
|
-
const acceptedIndexes = /* @__PURE__ */ new Set();
|
|
46431
|
-
if (accepted !== void 0) for (const record of accepted) {
|
|
46432
|
-
const match = /^workflow-links:(\d+)$/.exec(record.decisionId);
|
|
46433
|
-
const index = match === null ? -1 : Number(match[1]);
|
|
46434
|
-
const item = decisionInputs[index];
|
|
46435
|
-
if (!item || record.inputRef !== `${item.from} → ${item.to}`) throw new Error(`Decision does not match a researched link: ${record.decisionId}`);
|
|
46436
|
-
acceptedIndexes.add(index);
|
|
46437
|
-
}
|
|
46438
|
-
const actionItems = accepted === void 0 ? void 0 : state.items.filter((_, index) => acceptedIndexes.has(index));
|
|
46439
|
-
if (actionItems !== void 0 && actionItems.length > 0 && suggestionReport !== void 0) {
|
|
46440
|
-
const readSentences = dependencies.readSuggestionSentences ?? readCurrentSuggestionSentences;
|
|
46441
|
-
const current = /* @__PURE__ */ new Map();
|
|
46442
|
-
for (const item of actionItems) {
|
|
46443
|
-
const candidate = suggestionReport.candidates.find((entry) => entry.source === item.from && entry.destination === item.to && entry.anchor === item.anchor && entry.sentence === item.context);
|
|
46444
|
-
if (!candidate) throw new Error("Accepted link is absent from the supplied suggestions");
|
|
46445
|
-
for (const path of [candidate.source, candidate.destination]) if (!current.has(path)) current.set(path, await readSentences(suggestionReport.origin, path, input.options.allowPrivate === true, suggestionReport.maxBodyBytes));
|
|
46446
|
-
if (!current.get(candidate.source).includes(candidate.sentence) || !current.get(candidate.destination).includes(candidate.targetSentence)) throw new Error(`Suggestion is stale; served copy changed for ${candidate.source} → ${candidate.destination}. Regenerate links candidates --site.`);
|
|
46430
|
+
state = decodeResearchState(researched.state);
|
|
46431
|
+
} catch (firstError) {
|
|
46432
|
+
stage = "repair";
|
|
46433
|
+
try {
|
|
46434
|
+
const repaired = await host.continue(researched.sessionId, [
|
|
46435
|
+
`The previous ${spec.id} research JSON did not match the required state schema: ${firstError instanceof Error ? firstError.message : String(firstError)}.`,
|
|
46436
|
+
`Return only one corrected JSON object matching this schema: ${JSON.stringify(spec.stateJsonSchema)}.`,
|
|
46437
|
+
"Use only evidence already collected. Do not call tools, edit files, or add commentary."
|
|
46438
|
+
].join(" "), {
|
|
46439
|
+
skills: spec.skills,
|
|
46440
|
+
permissions: researchPermissions
|
|
46441
|
+
});
|
|
46442
|
+
retainResult(repaired);
|
|
46443
|
+
assertResearchReadOnly();
|
|
46444
|
+
state = decodeResearchState(repaired.state);
|
|
46445
|
+
researched = repaired;
|
|
46446
|
+
} catch (repairError) {
|
|
46447
|
+
throw new Error(`${spec.id} research state failed schema validation after one read-only repair.`, { cause: new AggregateError([firstError, repairError], "Research decode and repair failed", { cause: repairError }) });
|
|
46447
46448
|
}
|
|
46448
46449
|
}
|
|
46449
|
-
const
|
|
46450
|
-
|
|
46451
|
-
|
|
46452
|
-
|
|
46453
|
-
|
|
46454
|
-
|
|
46455
|
-
resolved: accepted,
|
|
46456
|
-
review: [],
|
|
46457
|
-
counts: {
|
|
46458
|
-
inputs: accepted.length,
|
|
46459
|
-
resolved: accepted.length,
|
|
46460
|
-
review: 0
|
|
46461
|
-
},
|
|
46462
|
-
verdicts: {
|
|
46463
|
-
add: accepted.filter((record) => record.verdict === "add").length,
|
|
46464
|
-
update: accepted.filter((record) => record.verdict === "update").length
|
|
46450
|
+
const decisionInputs = spec.decisionInputs(state);
|
|
46451
|
+
if (suggestionReport !== void 0) {
|
|
46452
|
+
const valid = new Set(suggestionReport.candidates.map((item) => `${item.source}\u0000${item.destination}\u0000${item.anchor}\u0000${item.sentence}`));
|
|
46453
|
+
for (const item of decisionInputs) {
|
|
46454
|
+
const link = item;
|
|
46455
|
+
if (!valid.has(`${link.from}\u0000${link.to}\u0000${link.anchor}\u0000${link.context}`)) throw new Error("Research proposed a link absent from the supplied suggestions");
|
|
46465
46456
|
}
|
|
46466
|
-
}
|
|
46467
|
-
const
|
|
46468
|
-
|
|
46469
|
-
skills: spec.skills,
|
|
46470
|
-
permissions: actionPermissions
|
|
46471
|
-
}) : researched;
|
|
46472
|
-
const gitAfter = inspectGit(input.root);
|
|
46473
|
-
const files = changedFiles(gitAtCheckpoint, gitAfter);
|
|
46474
|
-
if ((!spec.mutatesFiles || input.options.dryRun) && files.length > 0) throw new Error(`${spec.id} changed repository files while running in read-only mode: ${files.join(", ")}`);
|
|
46475
|
-
const run = {
|
|
46476
|
-
kind: "pagegraph-workflow-run",
|
|
46457
|
+
}
|
|
46458
|
+
const checkpoint = {
|
|
46459
|
+
kind: "pagegraph-workflow-research-checkpoint",
|
|
46477
46460
|
schemaVersion: 1,
|
|
46478
46461
|
id,
|
|
46479
46462
|
workflow: spec.id,
|
|
46480
46463
|
startedAt: started.toISOString(),
|
|
46481
|
-
|
|
46464
|
+
checkpointedAt: now().toISOString(),
|
|
46482
46465
|
project: {
|
|
46483
46466
|
root: input.root,
|
|
46484
46467
|
head: gitAtStart.head,
|
|
46485
|
-
dirtyAtStart: gitAtStart.dirty
|
|
46468
|
+
dirtyAtStart: gitAtStart.dirty,
|
|
46469
|
+
filesAtStart: gitAtStart.files
|
|
46486
46470
|
},
|
|
46487
46471
|
options: input.options,
|
|
46488
|
-
model: host.model,
|
|
46489
|
-
targets: {
|
|
46490
|
-
pages: evidence.graph.nodes.map((node) => node.path),
|
|
46491
|
-
queries: input.options.queries,
|
|
46492
|
-
kinds: input.options.kinds
|
|
46493
|
-
},
|
|
46494
46472
|
evidence: {
|
|
46495
46473
|
...suppliedEvidence,
|
|
46496
|
-
executor:
|
|
46474
|
+
executor: researched.executor
|
|
46497
46475
|
},
|
|
46498
|
-
|
|
46499
|
-
|
|
46500
|
-
files,
|
|
46501
|
-
providerCalls: acted.executor.calls.map((call) => call.tool)
|
|
46502
|
-
},
|
|
46503
|
-
result: mayAct ? acted.state : accepted !== void 0 ? {
|
|
46504
|
-
summary: "No link suggestions accepted.",
|
|
46505
|
-
outcome: "no-change",
|
|
46506
|
-
files: []
|
|
46507
|
-
} : state,
|
|
46476
|
+
state,
|
|
46477
|
+
decisionInputs,
|
|
46508
46478
|
opencode: {
|
|
46509
46479
|
agent: "seo",
|
|
46510
|
-
sessionId:
|
|
46511
|
-
|
|
46480
|
+
sessionId: researched.sessionId,
|
|
46481
|
+
model: host.model,
|
|
46482
|
+
transcript: researched.transcript
|
|
46512
46483
|
}
|
|
46513
46484
|
};
|
|
46514
|
-
|
|
46515
|
-
|
|
46516
|
-
|
|
46517
|
-
|
|
46485
|
+
const checkpointPath = writeResearchCheckpoint(input.root, runsDirectory, checkpoint);
|
|
46486
|
+
const gitAtCheckpoint = inspectGit(input.root);
|
|
46487
|
+
stage = "decide";
|
|
46488
|
+
try {
|
|
46489
|
+
const decisionReport = await (dependencies.decide ?? defaultDecide)(decisionInputs, spec);
|
|
46490
|
+
const accepted = spec.id === "improve.links" ? decisionReport.resolved.filter((record) => record.verdict === "add" || record.verdict === "update") : void 0;
|
|
46491
|
+
const acceptedIndexes = /* @__PURE__ */ new Set();
|
|
46492
|
+
if (accepted !== void 0) for (const record of accepted) {
|
|
46493
|
+
const match = /^workflow-links:(\d+)$/.exec(record.decisionId);
|
|
46494
|
+
const index = match === null ? -1 : Number(match[1]);
|
|
46495
|
+
const item = decisionInputs[index];
|
|
46496
|
+
if (!item || record.inputRef !== `${item.from} → ${item.to}`) throw new Error(`Decision does not match a researched link: ${record.decisionId}`);
|
|
46497
|
+
acceptedIndexes.add(index);
|
|
46498
|
+
}
|
|
46499
|
+
const actionItems = accepted === void 0 ? void 0 : state.items.filter((_, index) => acceptedIndexes.has(index));
|
|
46500
|
+
if (actionItems !== void 0 && actionItems.length > 0 && suggestionReport !== void 0) {
|
|
46501
|
+
const readSentences = dependencies.readSuggestionSentences ?? readCurrentSuggestionSentences;
|
|
46502
|
+
const current = /* @__PURE__ */ new Map();
|
|
46503
|
+
for (const item of actionItems) {
|
|
46504
|
+
const candidate = suggestionReport.candidates.find((entry) => entry.source === item.from && entry.destination === item.to && entry.anchor === item.anchor && entry.sentence === item.context);
|
|
46505
|
+
if (!candidate) throw new Error("Accepted link is absent from the supplied suggestions");
|
|
46506
|
+
for (const path of [candidate.source, candidate.destination]) if (!current.has(path)) current.set(path, await readSentences(suggestionReport.origin, path, input.options.allowPrivate === true, suggestionReport.maxBodyBytes));
|
|
46507
|
+
if (!current.get(candidate.source).includes(candidate.sentence) || !current.get(candidate.destination).includes(candidate.targetSentence)) throw new Error(`Suggestion is stale; served copy changed for ${candidate.source} → ${candidate.destination}. Regenerate links candidates --site.`);
|
|
46508
|
+
}
|
|
46509
|
+
}
|
|
46510
|
+
const actionState = actionItems === void 0 ? state : {
|
|
46511
|
+
...state,
|
|
46512
|
+
items: actionItems
|
|
46513
|
+
};
|
|
46514
|
+
const actionDecisions = accepted === void 0 ? decisionReport : {
|
|
46515
|
+
...decisionReport,
|
|
46516
|
+
resolved: accepted,
|
|
46517
|
+
review: [],
|
|
46518
|
+
counts: {
|
|
46519
|
+
inputs: accepted.length,
|
|
46520
|
+
resolved: accepted.length,
|
|
46521
|
+
review: 0
|
|
46522
|
+
},
|
|
46523
|
+
verdicts: {
|
|
46524
|
+
add: accepted.filter((record) => record.verdict === "add").length,
|
|
46525
|
+
update: accepted.filter((record) => record.verdict === "update").length
|
|
46526
|
+
}
|
|
46527
|
+
};
|
|
46528
|
+
const mayAct = spec.mutatesFiles && (actionItems === void 0 || actionItems.length > 0);
|
|
46529
|
+
if (mayAct) stage = "action";
|
|
46530
|
+
const acted = mayAct ? await host.continue(researched.sessionId, actionPrompt(spec, actionState, actionDecisions, mutation), {
|
|
46531
|
+
skills: spec.skills,
|
|
46532
|
+
permissions: actionPermissions
|
|
46533
|
+
}) : researched;
|
|
46534
|
+
retainResult(acted);
|
|
46535
|
+
const gitAfter = inspectGit(input.root);
|
|
46536
|
+
const files = changedFiles(gitAtCheckpoint, gitAfter);
|
|
46537
|
+
if ((!spec.mutatesFiles || input.options.dryRun) && files.length > 0) throw new Error(`${spec.id} changed repository files while running in read-only mode: ${files.join(", ")}`);
|
|
46538
|
+
const run = {
|
|
46539
|
+
kind: "pagegraph-workflow-run",
|
|
46540
|
+
schemaVersion: 1,
|
|
46541
|
+
id,
|
|
46542
|
+
workflow: spec.id,
|
|
46543
|
+
startedAt: started.toISOString(),
|
|
46544
|
+
finishedAt: now().toISOString(),
|
|
46545
|
+
project: {
|
|
46546
|
+
root: input.root,
|
|
46547
|
+
head: gitAtStart.head,
|
|
46548
|
+
dirtyAtStart: gitAtStart.dirty
|
|
46549
|
+
},
|
|
46550
|
+
options: input.options,
|
|
46551
|
+
model: host.model,
|
|
46552
|
+
targets: {
|
|
46553
|
+
pages: evidence.graph.nodes.map((node) => node.path),
|
|
46554
|
+
queries: input.options.queries,
|
|
46555
|
+
kinds: input.options.kinds
|
|
46556
|
+
},
|
|
46557
|
+
evidence: {
|
|
46558
|
+
...suppliedEvidence,
|
|
46559
|
+
executor: acted.executor
|
|
46560
|
+
},
|
|
46561
|
+
decisions: [decisionArtifact(spec, decisionInputs, decisionReport)],
|
|
46562
|
+
changes: {
|
|
46563
|
+
files,
|
|
46564
|
+
providerCalls: acted.executor.calls.map((call) => call.tool)
|
|
46565
|
+
},
|
|
46566
|
+
result: mayAct ? acted.state : accepted !== void 0 ? {
|
|
46567
|
+
summary: "No link suggestions accepted.",
|
|
46568
|
+
outcome: "no-change",
|
|
46569
|
+
files: []
|
|
46570
|
+
} : state,
|
|
46571
|
+
opencode: {
|
|
46572
|
+
agent: "seo",
|
|
46573
|
+
sessionId: acted.sessionId,
|
|
46574
|
+
transcript: acted.transcript
|
|
46575
|
+
}
|
|
46576
|
+
};
|
|
46577
|
+
return {
|
|
46578
|
+
run,
|
|
46579
|
+
directory: writeRunBundle(input.root, runsDirectory, run)
|
|
46580
|
+
};
|
|
46581
|
+
} catch (cause) {
|
|
46582
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
46583
|
+
throw new Error(`${message}\nResearch checkpoint: ${checkpointPath}`, { cause });
|
|
46584
|
+
}
|
|
46518
46585
|
} catch (cause) {
|
|
46519
|
-
|
|
46520
|
-
throw
|
|
46586
|
+
turnFailed = true;
|
|
46587
|
+
throw cause;
|
|
46588
|
+
} finally {
|
|
46589
|
+
try {
|
|
46590
|
+
await host.close();
|
|
46591
|
+
} catch (cause) {
|
|
46592
|
+
if (!turnFailed) throw cause;
|
|
46593
|
+
}
|
|
46521
46594
|
}
|
|
46522
|
-
}
|
|
46523
|
-
|
|
46595
|
+
} catch (cause) {
|
|
46596
|
+
const seen = /* @__PURE__ */ new Set();
|
|
46597
|
+
let current = cause;
|
|
46598
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
46599
|
+
seen.add(current);
|
|
46600
|
+
if (current instanceof WorkflowHostError) {
|
|
46601
|
+
sessionId = current.sessionId;
|
|
46602
|
+
transcript = current.transcript;
|
|
46603
|
+
break;
|
|
46604
|
+
}
|
|
46605
|
+
current = current.cause;
|
|
46606
|
+
}
|
|
46607
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
46608
|
+
let path;
|
|
46609
|
+
try {
|
|
46610
|
+
path = writeWorkflowFailure(input.root, runsDirectory, id, {
|
|
46611
|
+
kind: "pagegraph-workflow-failure",
|
|
46612
|
+
schemaVersion: 1,
|
|
46613
|
+
id,
|
|
46614
|
+
workflow: spec.id,
|
|
46615
|
+
stage,
|
|
46616
|
+
sessionId,
|
|
46617
|
+
transcript,
|
|
46618
|
+
cause: serializeCause(cause)
|
|
46619
|
+
});
|
|
46620
|
+
} catch (writeError) {
|
|
46621
|
+
if (cause instanceof Error) {
|
|
46622
|
+
cause.message += `\nCould not write failure artifact: ${writeError instanceof Error ? writeError.message : String(writeError)}`;
|
|
46623
|
+
throw cause;
|
|
46624
|
+
}
|
|
46625
|
+
throw new Error(`${message}\nCould not write failure artifact: ${String(writeError)}`, { cause });
|
|
46626
|
+
}
|
|
46627
|
+
throw new Error(`${message}\nFailure artifact: ${path}\nNext: ${stage === "acquire" ? "inspect that file; check the OpenCode preset and Executor plugin configuration" : stage === "decide" ? "inspect that file and research.json; check the decision provider configuration and retry" : "inspect the transcript in that file; rerun with --model <id> or raise workflows.opencode.timeoutMs"}`, { cause });
|
|
46524
46628
|
}
|
|
46525
46629
|
};
|
|
46526
46630
|
//#endregion
|
|
@@ -47557,13 +47661,15 @@ const robotsCommand = make$9("robots", {
|
|
|
47557
47661
|
description: "Disallow-all with no Sitemap line — the preview posture"
|
|
47558
47662
|
}]), withHandler(fnUntraced(function* ({ origin, indexable }) {
|
|
47559
47663
|
const config = yield* loadSeoConfig;
|
|
47560
|
-
const
|
|
47561
|
-
yield*
|
|
47664
|
+
const loaded = yield* scoped(acquireLoadedGraph(config));
|
|
47665
|
+
if ((config.disallow !== void 0 || config.contentSignal !== void 0 || config.directives !== void 0) && loaded.robots !== void 0) return yield* new SeoCliError({ message: "The graph loader supplies the robots policy; remove disallow, contentSignal, and directives from seo.config.ts." });
|
|
47666
|
+
const policy = loaded.robots ?? config;
|
|
47667
|
+
yield* printText(renderRobots(loaded.graph, {
|
|
47562
47668
|
origin: originOf(origin, config.origin),
|
|
47563
47669
|
indexable,
|
|
47564
|
-
disallow:
|
|
47565
|
-
contentSignal:
|
|
47566
|
-
directives:
|
|
47670
|
+
disallow: policy.disallow ?? [],
|
|
47671
|
+
contentSignal: policy.contentSignal,
|
|
47672
|
+
directives: policy.directives,
|
|
47567
47673
|
transform: config.transform
|
|
47568
47674
|
}));
|
|
47569
47675
|
})));
|
|
@@ -47606,7 +47712,7 @@ const staleCommand = make$9("stale", {
|
|
|
47606
47712
|
})));
|
|
47607
47713
|
//#endregion
|
|
47608
47714
|
//#region src/cli/commands/skills.ts
|
|
47609
|
-
const skills = { core: "---\nname: core\ndescription: Use when adding Pagegraph to a TanStack Start app or changing its SEO wiring — staticData.seo declarations, seoHead in route head(), typed related links, content collections, the seoRouteConfig coverage gate, seo.config.ts, sitemap and robots routes — or when a `pagegraph check`, coverage, or rendered-head failure needs diagnosing.\n---\n\n# Pagegraph and TanStack Start\n\nEach route declares its SEO policy once in `staticData.seo`; Pagegraph derives the sitemap, robots, breadcrumbs, JSON-LD, related links, and `pagegraph check` from those declarations. TanStack Start still owns routing and the document shell. This skill ships inside the Pagegraph CLI that printed it, so it matches that version. Run the app's installed binary (`bunx pagegraph` from the app root) so the guidance and the types agree.\n\nBefore editing, read the app's `package.json`, `src/routes/`, `src/routeTree.gen.ts`, any existing `seo.config.ts`, and `vite.config.ts`. Adapt the paths below to the app's layout.\n\n## Wire it up\n\n**1. Bind site identity and typed paths once.** Route files never repeat the origin or brand.\n\n```ts\n// src/lib/seo.ts\nimport { createSeo } from \"pagegraph/react\";\nimport type { FileRouteTypes } from \"../routeTree.gen\";\n\ndeclare module \"pagegraph\" {\n interface Register {\n paths: FileRouteTypes[\"fullPaths\"];\n kinds: \"page\" | \"article\" | \"hub\";\n }\n}\n\nexport const { seoHead } = createSeo({\n origin: \"https://example.com\",\n site: {\n name: \"Example\",\n logo: \"/logo.png\",\n publisherLogo: \"/publisher.png\",\n defaultImage: \"/og.png\",\n defaultAuthor: { name: \"Example Team\" },\n },\n organization: {\n description: \"What the company does.\",\n sameAs: [],\n contactPoint: { contactType: \"support\", email: \"support@example.com\" },\n },\n website: { searchPath: \"/search?q={search_term_string}\" },\n});\n```\n\nThe `Register` augmentation is what types `staticData.seo` on TanStack routes and checks `related` and `redirectTo` against real paths, so a renamed route becomes a compile error. `kinds` is the app's own taxonomy; nothing branches on it.\n\n**2. Declare policy on the route, content in `head()`.**\n\n```tsx\n// src/routes/(marketing)/pricing.tsx\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport { seoHead } from \"../../lib/seo\";\n\nexport const Route = createFileRoute(\"/(marketing)/pricing\")({\n staticData: {\n seo: {\n kind: \"page\",\n crumb: \"Pricing\",\n sitemap: { priority: 0.9, changeFrequency: \"monthly\" },\n related: [\"/features\"],\n link: { title: \"Pricing\", description: \"Simple volume pricing.\" },\n },\n },\n head: (ctx) =>\n seoHead(ctx, {\n title: \"Pricing — Example\",\n description: \"Simple volume pricing.\",\n }),\n component: PricingPage,\n});\n```\n\n- `staticData.seo` is static crawl policy and graph edges: `kind`, `crumb`, `sitemap` (or `false`), `robots`, `related`, `link`, `redirectTo`.\n- `seoHead(ctx, instance)` takes the full `title` (no suffix is added) and `description`, and optionally `article`, `faqs`, `service`, `itemList`, `jsonLd`, `canonicalPath`, and `breadcrumbs`. It returns `meta` and a canonical `links` entry for TanStack to render; the document shell must keep `<HeadContent />` and `<Scripts />`.\n- `link` is how other pages render a card for this route. Add it to every route that appears in someone's `related`.\n- Declare `related` only for links the page actually renders in its body.\n- `crumb` accepts `(match) => string` for dynamic segments, such as a title from `loaderData`.\n\n**3. Export one graph loader.** The CLI, sitemap, and robots all read it.\n\n```ts\n// src/lib/seo/graph.ts\nimport { buildSeoGraph } from \"pagegraph\";\nimport { routeTree } from \"../../routeTree.gen\";\nimport { posts } from \"../content/posts\"; // the app's own content source\n\nexport const loadSeoGraph = async () =>\n buildSeoGraph({\n routeTree,\n collections: [\n {\n route: \"/blog/$slug\",\n source: \"blog\",\n instances: posts.map((post) => ({\n path: `/blog/${post.slug}`,\n title: post.title,\n description: post.description,\n publishedAt: post.date,\n })),\n },\n ],\n });\n```\n\nA param route such as `/blog/$slug` never enters the sitemap itself; list its pages as a collection. Instances inherit the param route's `kind` and `sitemap`, so declare those on `/blog/$slug`. Omit `collections` when every public page is a static route.\n\n**4. Point the CLI at the loader** with `seo.config.ts` at the app root:\n\n```ts\n// seo.config.ts\nimport { defineSeoConfig, viteGraphLoader } from \"pagegraph/config\";\n\nexport default defineSeoConfig({\n origin: \"https://example.com\",\n disallow: [\"/dashboard\", \"/api\"],\n loadGraph: viteGraphLoader({\n root: import.meta.dirname,\n entry: \"/src/lib/seo/graph.ts\",\n exportName: \"loadSeoGraph\",\n }),\n});\n```\n\n`entry` is root-relative and evaluated in a headless Vite server, so aliases and content plugins resolve as they do in the app. Optional fields: `contentSignal`, `coverage` (`[{ path, minInbound }]`, contextual-link minimums enforced by `check`), `directives`, and `transform`.\n\n**5. Serve the projections.** In Start server routes, return `renderSitemap(graph, { origin, indexable })` and `renderRobots(graph, { origin, indexable, disallow, contentSignal })` from `pagegraph`. On preview deployments, pass `indexable: false` so robots.txt disallows everything; the sitemap body does not change.\n\n**6. Optionally gate coverage at build time.**\n\n```ts\n// vite.config.ts\nimport { fileURLToPath } from \"node:url\";\nimport { tanstackStart } from \"@tanstack/react-start/plugin/vite\";\nimport { defineConfig } from \"vite\";\nimport { seoRouteConfig } from \"pagegraph/vite\";\n\nexport default defineConfig({\n plugins: [\n tanstackStart(),\n // Runs its own route parse; it does not replace the Start plugin.\n seoRouteConfig({\n outputPath: fileURLToPath(new URL(\"./src/lib/route-config.ts\", import.meta.url)),\n publicGroups: [\"(marketing)\"],\n enforceCoverageIn: [\"(marketing)\"],\n alwaysDisallow: [\"/dashboard\", \"/api\"],\n }),\n ],\n});\n```\n\nThis plugin fails dev startup and `vite build` when a page leaf in an enforced group has neither `staticData` nor `head`. It writes `robotsExclusions` and `reservedSegments` to `outputPath`. Feed `robotsExclusions` into `seo.config.ts` `disallow` and `renderRobots` instead of repeating the list.\n\n## Rules that bite\n\n- `robots` values must be lowercase (`\"noindex, follow\"`). The sitemap projection matches `noindex` literally, so `check` rejects mixed case.\n- A route with `robots` containing `noindex` or with `redirectTo` must not declare a positive `sitemap`. Use `sitemap: false` or omit it.\n- A `related` target route without `link` fails `check`. Collection instances are exempt because their card uses the instance `title` and `description`.\n- Coverage enforcement sees only route files inside the named group folders. `route.tsx` layouts, `.ts` server routes, and ungrouped routes are not checked. List ungrouped public routes in `extraPublicRoutes`.\n- `seoRouteConfig` throws if the directory of `outputPath` does not exist. Point it outside `src/routes` so the route watcher does not loop.\n- The graph knows route declarations and collection instances, not `head()` output. Editorial title and description checks cover instances only. A route's rendered title, headings, and JSON-LD are verified only by inspecting HTML (`check --site`).\n- A FAQPage question or answer, or an Offer price, that is not in the server-rendered text fails `check --site`. Accordions must render closed panels into the HTML (for example `hidden=\"until-found\"`), not mount them on open.\n- Dates (`publishedAt`, `modifiedAt`, a route's `modifiedAt`) must be ISO 8601 (`2026-09-29`, or a date-time with an offset); they become the sitemap's `<lastmod>`, and `check` rejects any other form.\n- Keep indexable pages server-rendered or prerendered. An `ssr: false` route's head is not in the served HTML.\n\n## Verify\n\n| Changed | Run | Proves |\n| --- | --- | --- |\n| Declarations, collections, `seo.config.ts` | `pagegraph check` | No structural violations (exit 1 otherwise); editorial findings are reported but do not fail |\n| Headings, JSON-LD, robots meta, canonicals, page copy | `pagegraph check --site <url>` against a running server | Every declared page's rendered HTML matches its declaration and its structured data |\n| `modifiedAt` dates, `freshness` policy | `pagegraph stale` | The refresh queue and which pages are undated |\n| Graph shape | `pagegraph graph`, `pagegraph inspect /path` | Nodes, edges, and a page's sitemap status |\n| Sitemap or robots wiring | `pagegraph sitemap`, `pagegraph robots` | The projections from the declared graph |\n| `head()`, `seoHead` input, document shell | `pagegraph inspect <full-url> --live` against a running server | The served `<head>` and JSON-LD |\n| Rendered links vs `related` | `pagegraph links verify <url>` (add `--assert-coverage` for `coverage` rules) | What a crawler receives, not what was declared |\n| Coverage gate | `vite build` | Every enforced page leaf declares SEO |\n\nA passing `check` without `--site` says nothing about served HTML. Verify head and content changes with `check --site`, `inspect --live`, or a rendered-HTML test. Add `--json` when a script consumes the output. Stdout carries the data and stderr carries status. Check the exit code as well as the output.\n" };
|
|
47715
|
+
const skills = { core: "---\nname: core\ndescription: Use when adding Pagegraph to a TanStack Start app or changing its SEO wiring — the pagegraph() Vite plugin, staticData.seo declarations and seo.head, title templates, typed related links, content collections, the coverage gate, seo.config.ts, sitemap and robots routes — or when a `pagegraph check`, coverage, or rendered-head failure needs diagnosing.\n---\n\n# Pagegraph and TanStack Start\n\nEach route declares its SEO policy once in `staticData.seo`; Pagegraph derives the sitemap, robots, breadcrumbs, JSON-LD, related links, and `pagegraph check` from those declarations. TanStack Start still owns routing and the document shell. This skill ships inside the Pagegraph CLI that printed it, so it matches that version. Run the app's installed binary (`bunx pagegraph` from the app root) so the guidance and the types agree.\n\nBefore editing, read the app's `package.json`, `src/routes/`, `src/routeTree.gen.ts`, any existing `seo.config.ts`, and `vite.config.ts`. Adapt the paths below to the app's layout.\n\n## Wire it up\n\n**1. Bind site identity and typed paths once.** Route files never repeat the origin or brand.\n\n```ts\n// src/lib/seo.ts\nimport { createSeo } from \"pagegraph/react\";\nimport type { FileRouteTypes } from \"../routeTree.gen\";\n\ndeclare module \"pagegraph\" {\n interface Register {\n paths: FileRouteTypes[\"fullPaths\"];\n kinds: \"page\" | \"article\" | \"hub\";\n }\n}\n\nconst site = createSeo({\n origin: \"https://example.com\",\n site: {\n name: \"Example\",\n logo: \"/logo.png\",\n publisherLogo: \"/publisher.png\",\n defaultImage: \"/og.png\",\n defaultAuthor: { name: \"Example Team\" },\n },\n organization: {\n description: \"What the company does.\",\n sameAs: [],\n contactPoint: { contactType: \"support\", email: \"support@example.com\" },\n },\n website: { searchPath: \"/search?q={search_term_string}\" },\n});\n\nexport const seo = site; // seo.head for static routes\nexport const { seoHead } = site;\n```\n\nThe `Register` augmentation is what types `staticData.seo` on TanStack routes and checks `related` and `redirectTo` against real paths, so a renamed route becomes a compile error. `kinds` is the app's own taxonomy; nothing branches on it.\n\n**2. Declare each static page once.** `staticData.seo` holds the crawl policy and the head together; `seo.head` renders it.\n\n```tsx\n// src/routes/(marketing)/pricing.tsx\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport { seo } from \"../../lib/seo\";\n\nexport const Route = createFileRoute(\"/(marketing)/pricing\")({\n staticData: {\n seo: {\n kind: \"page\",\n crumb: \"Pricing\",\n sitemap: { priority: 0.9, changeFrequency: \"monthly\" },\n related: [\"/features\"],\n link: { title: \"Pricing\", description: \"Simple volume pricing.\" },\n head: { title: \"Pricing — Example\", description: \"Simple volume pricing.\", faqs: pricingFaqs },\n },\n },\n head: seo.head,\n component: PricingPage,\n});\n```\n\n- Policy fields: `kind`, `crumb`, `sitemap` (or `false`), `robots`, `related`, `link`, `redirectTo`, `modifiedAt`, `titleTemplate`.\n- `head` is the full `title` and `description`, plus optional `faqs`, `article`, `service`, `itemList`, `jsonLd`, `ogTitle`, `canonicalPath`, and `breadcrumbs`. The title, description, and FAQs land in `staticData.seo.head`, which the graph, `pageHeads`, and `check` read — the page and the graph cannot disagree.\n- Write `staticData` as an object literal and `head: seo.head`: the coverage gate reads both keys statically, and a helper call or an arrow typed with `AnyRouteMatch` in route options would make TanStack infer `any` search and loader types for the route tree.\n- `link` is how other pages render a card for this route. Add it to every route that appears in someone's `related`.\n- Declare `related` only for links the page actually renders in its body.\n- `crumb` accepts `(match) => string` for dynamic segments, such as a title from `loaderData`.\n\n**3. Dynamic routes keep `head()` and declare a title template.** A param route renders from `loaderData`, so it calls `seoHead`; its `titleTemplate` (`%s` is the page title) is applied by `seoHead` and by the graph to the route's collection instances, so the suffix lives in one place.\n\n```tsx\nexport const Route = createFileRoute(\"/(marketing)/blog/$slug\")({\n staticData: {\n seo: { kind: \"article\", titleTemplate: \"%s | Example Blog\", sitemap: { priority: 0.7, changeFrequency: \"weekly\" } },\n },\n head: (ctx) => (ctx.loaderData ? seoHead(ctx, { title: ctx.loaderData.title, description: ctx.loaderData.description }) : {}),\n});\n```\n\n**4. List content pages as collections.** A param route such as `/blog/$slug` never enters the sitemap itself; its pages come from a collections module. `contentCollection` accepts any pages with a `url` (Fumadocs `loader().getPages()` directly) and turns each entry's `related` into edges. Instances inherit the param route's `kind`, `sitemap`, and `titleTemplate`.\n\n```ts\n// src/lib/seo/collections.ts\nimport { contentCollection } from \"pagegraph\";\nimport { blogSource } from \"../blog-source\";\n\nexport const collections = () => [\n contentCollection({\n route: \"/blog/$slug\",\n source: \"blog\",\n pages: blogSource.getPages(),\n entry: (page) => ({\n title: page.data.title,\n description: page.data.description,\n publishedAt: page.data.date,\n related: page.data.related,\n }),\n }),\n];\n```\n\n**5. Register the plugin once.** It builds the graph inside the app's Vite pipeline (aliases, `define`s, and content plugins apply; no stubs) and ships it to the server runtime.\n\n```ts\n// vite.config.ts\nimport { fileURLToPath } from \"node:url\";\nimport { tanstackStart } from \"@tanstack/react-start/plugin/vite\";\nimport { pagegraph } from \"pagegraph/tanstack-start\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n plugins: [\n tanstackStart(),\n pagegraph({\n origin: \"https://example.com\",\n indexable: process.env.DEPLOY_ENV === \"production\",\n robots: { contentSignal: \"search=yes, ai-input=yes, ai-train=yes\" },\n collections: \"src/lib/seo/collections.ts\",\n routeConfig: {\n outputPath: fileURLToPath(new URL(\"./src/lib/route-config.ts\", import.meta.url)),\n publicGroups: [\"(marketing)\"],\n enforceCoverageIn: [\"(marketing)\"],\n alwaysDisallow: [\"/dashboard\", \"/api\"],\n },\n }),\n ],\n});\n```\n\n- The graph evaluates every route file that declares `staticData` in a Node environment named `pagegraph`, never `routeTree.gen.ts`. A route that imports a server-runtime-only module (`cloudflare:workers`) fails with its file name; if it is an app surface rather than a public page, add it to `exclude` (route-file globs relative to `src/routes`).\n- `routeConfig` is the coverage gate: dev and `vite build` fail when a page leaf in an enforced group has neither `staticData` nor `head`. Its `robotsExclusions` join `robots.disallow`.\n- `indexable: false` (previews, local) serves a disallow-all robots.txt with no Sitemap line.\n\n**6. Serve robots.txt and sitemap.xml.** They are routes, because their output depends on the deployment.\n\n```ts\n// src/routes/robots[.]txt.ts — sitemap[.]xml.ts is the same with sitemapXml\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport { robotsTxt } from \"pagegraph/tanstack-start/server\";\n\nexport const Route = createFileRoute(\"/robots.txt\")({ server: { handlers: { GET: robotsTxt } } });\n```\n\n`pagegraph/tanstack-start/server` also exports `seoGraph()` and `seoSite()` for other server code. Never import `pagegraph/tanstack-start`, `pagegraph/config`, `pagegraph/vite`, or `pagegraph/audit` from code a Worker or browser bundle reaches: under those runtimes they resolve to a stub that throws at import.\n\n**7. Point the CLI at the same graph** with `seo.config.ts` at the app root:\n\n```ts\n// seo.config.ts\nimport { defineSeoConfig } from \"pagegraph/config\";\nimport { tanstackStartGraph } from \"pagegraph/tanstack-start\";\n\nexport default defineSeoConfig({\n origin: \"https://example.com\",\n loadGraph: tanstackStartGraph({ root: import.meta.dirname }),\n});\n```\n\n`tanstackStartGraph` evaluates the app's Vite config the way the dev server does (command `serve`) in `production` mode, and returns the plugin's graph and robots policy. Optional fields: `coverage` (`[{ path, minInbound }]`, contextual-link minimums enforced by `check`), `content`, `freshness`, and `workflows`. Build-time head consumers use `loadPageHeads(tanstackStartGraph({ root }), { exclude })` from `pagegraph/config`.\n\n## Rules that bite\n\n- `robots` values must be lowercase (`\"noindex, follow\"`). The sitemap projection matches `noindex` literally, so `check` rejects mixed case.\n- A route with `robots` containing `noindex` or with `redirectTo` must not declare a positive `sitemap`. Use `sitemap: false` or omit it.\n- A `related` target route without `link` fails `check`. Collection instances are exempt because their card uses the instance `title` and `description`.\n- Coverage enforcement sees only route files inside the named group folders. `route.tsx` layouts, `.ts` server routes, and ungrouped routes are not checked. List ungrouped public routes in `extraPublicRoutes`.\n- `routeConfig.outputPath` must be in an existing directory outside `src/routes`, so the route watcher does not loop.\n- The graph knows declared heads (`staticData.seo.head`) and collection instances, not what a dynamic `head()` computes at request time. A route's rendered headings and JSON-LD are verified only by inspecting HTML (`check --site`).\n- A FAQPage question or answer, or an Offer price, that is not in the server-rendered text fails `check --site`. Accordions must render closed panels into the HTML (for example `hidden=\"until-found\"`), not mount them on open.\n- Dates (`publishedAt`, `modifiedAt`, a route's `modifiedAt`) must be ISO 8601 (`2026-09-29`, or a date-time with an offset); they become the sitemap's `<lastmod>`, and `check` rejects any other form.\n- Keep indexable pages server-rendered or prerendered. An `ssr: false` route's head is not in the served HTML.\n\n## Verify\n\n| Changed | Run | Proves |\n| --- | --- | --- |\n| Declarations, collections, the plugin, `seo.config.ts` | `pagegraph check` | No structural violations (exit 1 otherwise); editorial findings are reported but do not fail |\n| Headings, JSON-LD, robots meta, canonicals, page copy | `pagegraph check --site <url>` against a running server | Every declared page's rendered HTML matches its declaration and its structured data |\n| `modifiedAt` dates, `freshness` policy | `pagegraph stale` | The refresh queue and which pages are undated |\n| Graph shape | `pagegraph graph`, `pagegraph inspect /path` | Nodes, edges, and a page's sitemap status |\n| Sitemap or robots wiring | `pagegraph sitemap`, `pagegraph robots` | The projections from the declared graph |\n| Declared heads, `seoHead` input, document shell | `pagegraph inspect <full-url> --live` against a running server | The served `<head>` and JSON-LD |\n| Rendered links vs `related` | `pagegraph links verify <url>` (add `--assert-coverage` for `coverage` rules) | What a crawler receives, not what was declared |\n| Coverage gate | `vite build` | Every enforced page leaf declares SEO |\n\nA passing `check` without `--site` says nothing about served HTML. Verify head and content changes with `check --site`, `inspect --live`, or a rendered-HTML test. Add `--json` when a script consumes the output. Stdout carries the data and stderr carries status. Check the exit code as well as the output.\n" };
|
|
47610
47716
|
const nameArgument = String$1("name").pipe(withDescription("Bundled skill name from `pagegraph skills list`"));
|
|
47611
47717
|
const listCommand = make$9("list", { json: jsonFlag }).pipe(withDescription$1("List skills embedded in this Pagegraph CLI"), withExamples([{
|
|
47612
47718
|
command: "pagegraph skills list",
|