@warpgogol/forge 2.8.0 → 2.8.2
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/AGENTS.md +1 -1
- package/README.md +23 -13
- package/README.uk.md +19 -12
- package/dist/os/session/handlers/validate.d.ts.map +1 -1
- package/dist/os/session/handlers/validate.js +23 -1
- package/dist/os/session/handlers/validate.js.map +1 -1
- package/dist/os/session/types.d.ts +31 -1
- package/dist/os/session/types.d.ts.map +1 -1
- package/dist/os/session/types.js +7 -0
- package/dist/os/session/types.js.map +1 -1
- package/os/adr/adr.module.ts +158 -0
- package/os/adr/frontmatter-io.ts +80 -0
- package/os/adr/handlers/archive.ts +244 -0
- package/os/adr/handlers/implement-stamp.ts +331 -0
- package/os/adr/handlers/list-create.ts +207 -0
- package/os/adr/handlers/validate.test.ts +232 -0
- package/os/adr/handlers/validate.ts +429 -0
- package/os/adr/index.ts +33 -0
- package/os/adr/types.ts +141 -0
- package/os/audit/audit.module.ts +48 -0
- package/os/audit/frontmatter-io.ts +84 -0
- package/os/audit/handlers/archive.ts +239 -0
- package/os/audit/index.ts +23 -0
- package/os/audit/types.ts +29 -0
- package/os/compass/compass.module.ts +186 -0
- package/os/compass/handlers/compass-audit-handler.ts +382 -0
- package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
- package/os/compass/handlers/compass-inventory-handler.ts +294 -0
- package/os/compass/handlers/compass-inventory.ts +521 -0
- package/os/compass/handlers/git-revision.ts +137 -0
- package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
- package/os/compass/handlers/resolve-scan-root.ts +79 -0
- package/os/compass/index.ts +33 -0
- package/os/core/core.module.ts +948 -0
- package/os/core/handlers/assets-check.ts +122 -0
- package/os/core/handlers/assets-helpers.ts +174 -0
- package/os/core/handlers/assets-list.ts +99 -0
- package/os/core/handlers/build.ts +155 -0
- package/os/core/handlers/determinism-check.ts +354 -0
- package/os/core/handlers/dev.ts +127 -0
- package/os/core/handlers/invariant-engine.test.ts +681 -0
- package/os/core/handlers/knowledge-compact.ts +248 -0
- package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
- package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
- package/os/core/handlers/note-link-validate.test.ts +117 -0
- package/os/core/handlers/note-orphan-detect.test.ts +94 -0
- package/os/core/handlers/package-health.test.ts +226 -0
- package/os/core/handlers/package-health.ts +231 -0
- package/os/core/handlers/pinned-check.ts +200 -0
- package/os/core/handlers/pinned-init.ts +333 -0
- package/os/core/handlers/pinned-types.ts +50 -0
- package/os/core/handlers/pinned-validate.ts +301 -0
- package/os/core/handlers/profile-resolve.ts +92 -0
- package/os/core/handlers/release-prepare.ts +294 -0
- package/os/core/handlers/release-publish.ts +211 -0
- package/os/core/handlers/validate.ts +262 -0
- package/os/core/handlers/workspace-deps.ts +55 -0
- package/os/core/index.ts +13 -0
- package/os/exploration/exploration.module.ts +90 -0
- package/os/exploration/frontmatter-io.ts +77 -0
- package/os/exploration/handlers/archive.ts +159 -0
- package/os/exploration/handlers/list.ts +77 -0
- package/os/exploration/handlers/show.ts +107 -0
- package/os/exploration/index.ts +25 -0
- package/os/exploration/types.ts +65 -0
- package/os/mission/handlers/archive.test.ts +391 -0
- package/os/mission/handlers/archive.ts +453 -0
- package/os/mission/index.ts +1 -0
- package/os/mission/mission.module.ts +48 -0
- package/os/mission/types.ts +41 -0
- package/os/naming/index.ts +14 -0
- package/os/naming/naming-convention.test.ts +163 -0
- package/os/naming/naming-convention.ts +378 -0
- package/os/naming/naming.module.ts +36 -0
- package/os/notes/index.ts +10 -0
- package/os/notes/notes.module.ts +109 -0
- package/os/plan/frontmatter-io.ts +82 -0
- package/os/plan/handlers/archive.ts +238 -0
- package/os/plan/index.ts +23 -0
- package/os/plan/plan.module.ts +48 -0
- package/os/plan/types.ts +29 -0
- package/os/program/discovery.ts +294 -0
- package/os/program/handlers/complete.ts +419 -0
- package/os/program/handlers/lease.ts +433 -0
- package/os/program/handlers/seal.ts +332 -0
- package/os/program/handlers/validate.ts +213 -0
- package/os/program/lease.ts +156 -0
- package/os/program/program.module.ts +212 -0
- package/os/program/schemas.ts +218 -0
- package/os/program/state.ts +264 -0
- package/os/rfc/acceptance.ts +344 -0
- package/os/rfc/decision-log.ts +310 -0
- package/os/rfc/dna-trace.ts +280 -0
- package/os/rfc/frontmatter-io.test.ts +132 -0
- package/os/rfc/frontmatter-io.ts +130 -0
- package/os/rfc/handlers/archive.ts +300 -0
- package/os/rfc/handlers/check.ts +238 -0
- package/os/rfc/handlers/implement-stamp.ts +515 -0
- package/os/rfc/handlers/index-graph.ts +237 -0
- package/os/rfc/handlers/lifecycle.test.ts +127 -0
- package/os/rfc/handlers/lifecycle.ts +249 -0
- package/os/rfc/handlers/list-create.ts +296 -0
- package/os/rfc/handlers/pipeline-status.ts +173 -0
- package/os/rfc/handlers/shared.ts +121 -0
- package/os/rfc/handlers/supersede-propose.ts +241 -0
- package/os/rfc/handlers/validate-rules.test.ts +891 -0
- package/os/rfc/handlers/validate-rules.ts +984 -0
- package/os/rfc/handlers/validate.ts +166 -0
- package/os/rfc/handlers.ts +19 -0
- package/os/rfc/index.ts +97 -0
- package/os/rfc/rfc.module.ts +416 -0
- package/os/rfc/types.ts +596 -0
- package/os/rfc/verification-evidence.ts +244 -0
- package/os/session/atif-parser.ts +166 -0
- package/os/session/frontmatter-io.ts +163 -0
- package/os/session/handlers/archive.ts +288 -0
- package/os/session/handlers/list.ts +174 -0
- package/os/session/handlers/save.ts +348 -0
- package/os/session/handlers/validate.ts +250 -0
- package/os/session/index.ts +50 -0
- package/os/session/session.module.ts +126 -0
- package/os/session/types.ts +203 -0
- package/os/spec/live-spec-list-show-validate.test.ts +257 -0
- package/os/spec/live-spec-list.ts +78 -0
- package/os/spec/live-spec-merge.test.ts +261 -0
- package/os/spec/live-spec-merge.ts +413 -0
- package/os/spec/live-spec-show.ts +117 -0
- package/os/spec/live-spec-types.ts +93 -0
- package/os/spec/live-spec-validate.ts +170 -0
- package/os/spec/spec-materialize.ts +390 -0
- package/os/spec/spec-schema.ts +174 -0
- package/os/spec/spec-status.ts +284 -0
- package/os/spec/spec-validate.ts +513 -0
- package/os/spec/spec.module.ts +135 -0
- package/os/werkstatt/handlers/lock.ts +168 -0
- package/os/werkstatt/handlers/schema.ts +47 -0
- package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
- package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
- package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
- package/os/werkstatt/index.ts +39 -0
- package/os/werkstatt/werkstatt.module.ts +62 -0
- package/os/workflow/handlers.ts +304 -0
- package/os/workflow/index.ts +28 -0
- package/os/workflow/types.ts +99 -0
- package/os/workflow/workflow.module.ts +54 -0
- package/package.json +3 -3
- package/profiles/godot-csharp.yaml +4 -4
- package/profiles/knowledge-typescript-turborepo.yaml +110 -0
- package/profiles/phaser-turborepo.yaml +5 -5
- package/skills/_shared/fo-session-summary.md +132 -16
- package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
- package/skills/fo/fo-doc-audit/SKILL.md +6 -6
- package/skills/fo/fo-handoff/SKILL.md +5 -0
- package/skills/fo/fo-idea/SKILL.md +1 -1
- package/skills/fo/fo-review/SKILL.md +1 -1
- package/skills/fo/fo-session-retro/SKILL.md +8 -0
- package/skills/fo/fo-session-save/SKILL.md +8 -0
- package/src/cli-output.ts +59 -0
- package/src/config/__tests__/resolve-terminology.test.ts +45 -0
- package/src/config/forge-config.ts +524 -0
- package/src/forge-module.ts +39 -0
- package/src/index.ts +172 -0
- package/src/knowledge/__tests__/promote.test.ts +219 -0
- package/src/knowledge/__tests__/serialize.test.ts +192 -0
- package/src/knowledge/budgets.ts +202 -0
- package/src/knowledge/compact.ts +405 -0
- package/src/knowledge/index.ts +53 -0
- package/src/knowledge/parse.ts +227 -0
- package/src/knowledge/promote.ts +156 -0
- package/src/knowledge/schema.ts +112 -0
- package/src/knowledge/serialize.ts +72 -0
- package/src/migration-adapters/__tests__/ignored-files.test.ts +51 -0
- package/src/migration-adapters/__tests__/registry.test.ts +131 -0
- package/src/migration-adapters/__tests__/types.test.ts +86 -0
- package/src/migration-adapters/git-utils.ts +71 -0
- package/src/migration-adapters/ignored-files.ts +221 -0
- package/src/migration-adapters/index.ts +21 -0
- package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
- package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
- package/src/migration-adapters/registry.ts +61 -0
- package/src/migration-adapters/types.ts +68 -0
- package/src/onboarding/__tests__/workspace-discovery.test.ts +134 -0
- package/src/onboarding/agents-generate.ts +443 -0
- package/src/onboarding/create.ts +427 -0
- package/src/onboarding/doctor.ts +1297 -0
- package/src/onboarding/init.ts +337 -0
- package/src/onboarding/invariant-engine.ts +448 -0
- package/src/onboarding/memory-scaffold.ts +189 -0
- package/src/onboarding/nested-agents-generate.ts +93 -0
- package/src/onboarding/nested-agents-templates.ts +233 -0
- package/src/onboarding/profile-validate.ts +139 -0
- package/src/onboarding/scaffold-project.ts +299 -0
- package/src/onboarding/scaffold.ts +143 -0
- package/src/onboarding/upgrade.ts +558 -0
- package/src/onboarding/workspace-discovery.ts +167 -0
- package/src/profiles/__tests__/profile-schema.test.ts +228 -0
- package/src/profiles/__tests__/stack-profile.test.ts +190 -0
- package/src/profiles/__tests__/terminology-utils.test.ts +71 -0
- package/src/profiles/profile-schema.ts +350 -0
- package/src/profiles/stack-profile.ts +188 -0
- package/src/profiles/terminology-utils.ts +36 -0
- package/src/registry.ts +185 -0
- package/src/skill-schema.ts +37 -0
- package/src/tests/acceptance-criteria.test.ts +99 -0
- package/src/tests/adr-implement-stamp.test.ts +368 -0
- package/src/tests/agents-generate-domain.test.ts +317 -0
- package/src/tests/agents-generate.test.ts +458 -0
- package/src/tests/bindings-schema.test.ts +314 -0
- package/src/tests/budgets.test.ts +213 -0
- package/src/tests/cli-output.test.ts +196 -0
- package/src/tests/compact.test.ts +796 -0
- package/src/tests/create.test.ts +289 -0
- package/src/tests/doctor-autonomy.test.ts +98 -0
- package/src/tests/doctor-bindings.test.ts +286 -0
- package/src/tests/doctor-domain.test.ts +113 -0
- package/src/tests/exploration-handlers.test.ts +256 -0
- package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
- package/src/tests/forge-config.test.ts +420 -0
- package/src/tests/fs-atomic.test.ts +96 -0
- package/src/tests/fs.test.ts +103 -0
- package/src/tests/generated-marker.test.ts +161 -0
- package/src/tests/hash.test.ts +37 -0
- package/src/tests/implement-stamp.test.ts +505 -0
- package/src/tests/init-bindings.test.ts +183 -0
- package/src/tests/knowledge-parse.test.ts +307 -0
- package/src/tests/knowledge-pbt.test.ts +164 -0
- package/src/tests/memory-scaffold.test.ts +192 -0
- package/src/tests/migration-adapters.test.ts +480 -0
- package/src/tests/package-files.test.ts +54 -0
- package/src/tests/pinned-check.test.ts +194 -0
- package/src/tests/pinned-init.test.ts +126 -0
- package/src/tests/profile-schema.test.ts +194 -0
- package/src/tests/profile-validate.test.ts +120 -0
- package/src/tests/program-lease.test.ts +245 -0
- package/src/tests/program-paths-extended.test.ts +117 -0
- package/src/tests/program-paths.test.ts +98 -0
- package/src/tests/program-property.test.ts +139 -0
- package/src/tests/program-schemas.test.ts +445 -0
- package/src/tests/program-spec-node.test.ts +250 -0
- package/src/tests/promote.test.ts +481 -0
- package/src/tests/registry.test.ts +73 -0
- package/src/tests/scaffold-project.test.ts +150 -0
- package/src/tests/session-handlers.test.ts +503 -0
- package/src/tests/session-pbt.test.ts +190 -0
- package/src/tests/skill-schema.test.ts +146 -0
- package/src/tests/skill-validate-knowledge.test.ts +26 -0
- package/src/tests/skill-validate.test.ts +254 -0
- package/src/tests/stack-profile.test.ts +221 -0
- package/src/tests/string-utils.test.ts +42 -0
- package/src/tests/upgrade.test.ts +428 -0
- package/src/tests/werkstatt-lock.test.ts +343 -0
- package/src/tests/workspace-discovery-domain.test.ts +112 -0
- package/src/tests/workspace-discovery.test.ts +135 -0
- package/src/types.ts +221 -0
- package/src/utils/__tests__/fs-idempotent.test.ts +73 -0
- package/src/utils/__tests__/fs-trash.test.ts +45 -0
- package/src/utils/fs-atomic.ts +93 -0
- package/src/utils/fs-idempotent.ts +41 -0
- package/src/utils/fs-trash-sync.ts +37 -0
- package/src/utils/fs-trash.ts +24 -0
- package/src/utils/fs.ts +74 -0
- package/src/utils/generated-marker.ts +166 -0
- package/src/utils/hash.ts +19 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/string-utils.ts +19 -0
- package/src/validators/__tests__/note-orphan-detect.test.ts +94 -0
- package/src/validators/note-frontmatter-validate.ts +142 -0
- package/src/validators/note-link-validate.ts +145 -0
- package/src/validators/note-orphan-detect.ts +146 -0
- package/src/validators/port-validate.ts +97 -0
- package/src/validators/skill-validate.ts +831 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Canonical GENERATED_MARKER constant and advisory header builder for
|
|
4
|
+
generated-file governance. Moved from @warpgogol/site-kernel to forge as canonical
|
|
5
|
+
source (dependency inversion).</purpose>
|
|
6
|
+
<non-goals>
|
|
7
|
+
<item>Do not perform file I/O — operates on in-memory content strings only.</item>
|
|
8
|
+
</non-goals>
|
|
9
|
+
</MODULE_CONTRACT>
|
|
10
|
+
<CHANGE_SUMMARY>
|
|
11
|
+
<item>Moved from @warpgogol/site-kernel/generated-marker to forge as canonical source.</item>
|
|
12
|
+
<item>Added optional commandPrefix to GeneratedHeaderInput — defaults to "forge" for autonomous mode; site-kernel passes "pnpm exec werkstatt run".</item>
|
|
13
|
+
</CHANGE_SUMMARY>
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const GENERATED_MARKER =
|
|
17
|
+
"GENERATED. Do not change this line unless the file contains project specific changes.";
|
|
18
|
+
|
|
19
|
+
export function hasGeneratedMarker(content: string): boolean {
|
|
20
|
+
return content.includes(GENERATED_MARKER);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface StripGeneratedMarkerResult {
|
|
24
|
+
changed: boolean;
|
|
25
|
+
content: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function escapeRegex(str: string): string {
|
|
29
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DO_NOT_EDIT_LINE = "DO NOT EDIT THIS FILE. Changes are overwritten on the next build.";
|
|
33
|
+
|
|
34
|
+
export function stripGeneratedMarker(content: string): StripGeneratedMarkerResult {
|
|
35
|
+
const raw = GENERATED_MARKER;
|
|
36
|
+
let result = content;
|
|
37
|
+
let changed = false;
|
|
38
|
+
|
|
39
|
+
const applyAll = (patterns: RegExp[]): void => {
|
|
40
|
+
for (const pattern of patterns) {
|
|
41
|
+
const next = result.replace(pattern, "");
|
|
42
|
+
if (next !== result) {
|
|
43
|
+
changed = true;
|
|
44
|
+
result = next;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
applyAll([
|
|
50
|
+
new RegExp(`<!--[ \\t]*\\n[\\s\\S]*?${escapeRegex(raw)}[\\s\\S]*?-->\\n?`, "g"),
|
|
51
|
+
new RegExp(`/\\*[ \\t]*\\n[\\s\\S]*?${escapeRegex(raw)}[\\s\\S]*?\\*/\\n?`, "g"),
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
for (const token of ["//", "#"]) {
|
|
55
|
+
const t = escapeRegex(token);
|
|
56
|
+
const advisoryLine = (suffix: string): string => `(?:[ \\t]*${t}[ \\t]*${suffix}\\n)?`;
|
|
57
|
+
const pattern = new RegExp(
|
|
58
|
+
`^[ \\t]*${t}[ \\t]*${escapeRegex(raw)}\\n` +
|
|
59
|
+
advisoryLine(escapeRegex(DO_NOT_EDIT_LINE)) +
|
|
60
|
+
advisoryLine("Owner command:.*") +
|
|
61
|
+
advisoryLine("Edit instead:.*") +
|
|
62
|
+
advisoryLine("Regenerate:.*"),
|
|
63
|
+
"gm",
|
|
64
|
+
);
|
|
65
|
+
applyAll([pattern]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
applyAll([
|
|
69
|
+
new RegExp(`^\\s*<!--\\s*${escapeRegex(raw)}\\s*-->\\n?`, "gm"),
|
|
70
|
+
new RegExp(`^\\s*//\\s*${escapeRegex(raw)}\\n?`, "gm"),
|
|
71
|
+
new RegExp(`^\\s*/\\*\\s*${escapeRegex(raw)}\\s*\\*/\\n?`, "gm"),
|
|
72
|
+
new RegExp(`^\\s*#\\s*${escapeRegex(raw)}\\n?`, "gm"),
|
|
73
|
+
new RegExp(`^\\s*${escapeRegex(raw)}\\n?`, "gm"),
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
if (result.includes(raw)) {
|
|
77
|
+
const next = result.split(raw).join("");
|
|
78
|
+
if (next !== result) {
|
|
79
|
+
changed = true;
|
|
80
|
+
result = next;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { changed, content: result };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface GeneratedHeaderInput {
|
|
88
|
+
filePath: string;
|
|
89
|
+
ownerCommand: string;
|
|
90
|
+
templatePath?: string;
|
|
91
|
+
site?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Command prefix for the regenerate hint. Defaults to "forge" (autonomous mode).
|
|
94
|
+
* Site-kernel callers pass "pnpm exec werkstatt run" for Warpgogol context.
|
|
95
|
+
*/
|
|
96
|
+
commandPrefix?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
type GeneratedHeaderCommentStyle = "line-slash" | "line-hash" | "block-html" | "block-css";
|
|
100
|
+
|
|
101
|
+
function commentStyleForPath(filePath: string): GeneratedHeaderCommentStyle {
|
|
102
|
+
const lower = filePath.toLowerCase().replace(/\\/g, "/");
|
|
103
|
+
const base = lower.slice(lower.lastIndexOf("/") + 1);
|
|
104
|
+
if (
|
|
105
|
+
base === ".gitattributes" ||
|
|
106
|
+
base === "_headers" ||
|
|
107
|
+
base === "_redirects" ||
|
|
108
|
+
base.endsWith(".txt") ||
|
|
109
|
+
base.endsWith(".yml") ||
|
|
110
|
+
base.endsWith(".yaml")
|
|
111
|
+
) {
|
|
112
|
+
return "line-hash";
|
|
113
|
+
}
|
|
114
|
+
if (base.endsWith(".md")) return "block-html";
|
|
115
|
+
if (base.endsWith(".css")) return "block-css";
|
|
116
|
+
return "line-slash";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function buildGeneratedHeader(input: GeneratedHeaderInput): string {
|
|
120
|
+
const style = commentStyleForPath(input.filePath);
|
|
121
|
+
const prefix = input.commandPrefix ?? "forge";
|
|
122
|
+
const regenerateCommand = input.site
|
|
123
|
+
? `${prefix} ${input.ownerCommand} --site ${input.site}`
|
|
124
|
+
: `${prefix} ${input.ownerCommand}`;
|
|
125
|
+
const editInstead = input.templatePath
|
|
126
|
+
? `Edit instead: ${input.templatePath}`
|
|
127
|
+
: `Edit instead: the ${input.ownerCommand} generator source (not this file).`;
|
|
128
|
+
|
|
129
|
+
const lines = [
|
|
130
|
+
GENERATED_MARKER,
|
|
131
|
+
DO_NOT_EDIT_LINE,
|
|
132
|
+
`Owner command: ${input.ownerCommand}`,
|
|
133
|
+
editInstead,
|
|
134
|
+
`Regenerate: ${regenerateCommand}`,
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
switch (style) {
|
|
138
|
+
case "line-slash":
|
|
139
|
+
return `${lines.map((line) => `// ${line}`).join("\n")}\n`;
|
|
140
|
+
case "line-hash":
|
|
141
|
+
return `${lines.map((line) => `# ${line}`).join("\n")}\n`;
|
|
142
|
+
case "block-css":
|
|
143
|
+
return `/*\n${lines.map((line) => ` ${line}`).join("\n")}\n*/\n`;
|
|
144
|
+
case "block-html":
|
|
145
|
+
return `<!--\n${lines.map((line) => ` ${line}`).join("\n")}\n-->\n`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function isGeneratedMarkerTextCandidate(filePath: string): boolean {
|
|
150
|
+
const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
|
|
151
|
+
const TEXT_EXTS = new Set([
|
|
152
|
+
".txt",
|
|
153
|
+
".xml",
|
|
154
|
+
".md",
|
|
155
|
+
".yml",
|
|
156
|
+
".yaml",
|
|
157
|
+
".css",
|
|
158
|
+
".js",
|
|
159
|
+
".mjs",
|
|
160
|
+
".ts",
|
|
161
|
+
".json",
|
|
162
|
+
".html",
|
|
163
|
+
".astro",
|
|
164
|
+
]);
|
|
165
|
+
return TEXT_EXTS.has(ext);
|
|
166
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Trivial hash utility — inlined from @warpgogol/fingerprint to avoid dependency.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not add non-hash utilities here — use dedicated utility modules.</item>
|
|
6
|
+
<item>Do not introduce @warpgogol/* imports — this package must remain dependency-free.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>Initial inline of byteHash from @warpgogol/fingerprint/primitives.</item>
|
|
11
|
+
</CHANGE_SUMMARY>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
|
|
16
|
+
export function byteHash(bytes: Uint8Array | string): string {
|
|
17
|
+
const input = typeof bytes === "string" ? Buffer.from(bytes, "utf8") : bytes;
|
|
18
|
+
return `sha256:${createHash("sha256").update(input).digest("hex")}`;
|
|
19
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Barrel export for forge utilities — canonical, autonomous, no @warpgogol/* dependencies.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not add non-utility exports here — use the appropriate forge module.</item>
|
|
6
|
+
<item>Do not re-export from @warpgogol/* packages — this package must remain dependency-free.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>Initial utility barrel: fs-atomic, generated-marker, string-utils, fs, hash.</item>
|
|
11
|
+
</CHANGE_SUMMARY>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export { writeFileAtomic, type WriteFileAtomicOptions } from "./fs-atomic.ts";
|
|
15
|
+
export { writeFileIfChanged } from "./fs-idempotent.ts";
|
|
16
|
+
export {
|
|
17
|
+
GENERATED_MARKER,
|
|
18
|
+
hasGeneratedMarker,
|
|
19
|
+
stripGeneratedMarker,
|
|
20
|
+
buildGeneratedHeader,
|
|
21
|
+
isGeneratedMarkerTextCandidate,
|
|
22
|
+
type GeneratedHeaderInput,
|
|
23
|
+
type StripGeneratedMarkerResult,
|
|
24
|
+
} from "./generated-marker.ts";
|
|
25
|
+
export { toKebabCase } from "./string-utils.ts";
|
|
26
|
+
export { collectFiles, fileExists, type CollectFilesOptions } from "./fs.ts";
|
|
27
|
+
export { byteHash } from "./hash.ts";
|
|
28
|
+
export { trashPath } from "./fs-trash.ts";
|
|
29
|
+
export { trashSync } from "./fs-trash-sync.ts";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Trivial string utilities — inlined from @warpgogol/share to avoid dependency.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not add non-string utilities here — use dedicated utility modules.</item>
|
|
6
|
+
<item>Do not introduce @warpgogol/* imports — this package must remain dependency-free.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>Initial inline of toKebabCase from @warpgogol/werkstatt-shared/share/string-utils.</item>
|
|
11
|
+
</CHANGE_SUMMARY>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export function toKebabCase(value: string): string {
|
|
15
|
+
return value
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
18
|
+
.replace(/^-+|-+$/g, "");
|
|
19
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { runNoteOrphanDetect } from "../note-orphan-detect.ts";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
let tempDir: string;
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
tempDir = await mkdtemp(join(tmpdir(), "forge-orphan-"));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
async function createVaultFile(vaultDir: string, relPath: string, content: string) {
|
|
18
|
+
const fullPath = join(vaultDir, relPath);
|
|
19
|
+
const dir = join(fullPath, "..");
|
|
20
|
+
await mkdir(dir, { recursive: true });
|
|
21
|
+
await writeFile(fullPath, content, "utf8");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("runNoteOrphanDetect", () => {
|
|
25
|
+
it("returns empty when vault directory not found", () => {
|
|
26
|
+
const result = runNoteOrphanDetect(
|
|
27
|
+
{ flags: { "vault-dir": "nonexistent" } },
|
|
28
|
+
{ workspaceRoot: tempDir },
|
|
29
|
+
);
|
|
30
|
+
expect(result.data?.count).toBe(0);
|
|
31
|
+
expect(result.data?.orphans).toEqual([]);
|
|
32
|
+
expect(result.exitCode).toBe(0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("detects orphan note with zero inbound links", async () => {
|
|
36
|
+
const vaultDir = join(tempDir, "vault");
|
|
37
|
+
await mkdir(vaultDir, { recursive: true });
|
|
38
|
+
await createVaultFile(vaultDir, "linked.md", "# Linked\n\n[[target]]");
|
|
39
|
+
await createVaultFile(vaultDir, "target.md", "# Target");
|
|
40
|
+
await createVaultFile(vaultDir, "orphan.md", "# Orphan — nobody links to me");
|
|
41
|
+
|
|
42
|
+
const result = runNoteOrphanDetect(
|
|
43
|
+
{ flags: { "vault-dir": "vault" } },
|
|
44
|
+
{ workspaceRoot: tempDir },
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const orphanFiles = result.data?.orphans.map((o) => o.file) ?? [];
|
|
48
|
+
expect(orphanFiles).toContain("vault/orphan.md");
|
|
49
|
+
expect(orphanFiles).not.toContain("vault/target.md");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("does not flag notes that are linked via alias", async () => {
|
|
53
|
+
const vaultDir = join(tempDir, "vault");
|
|
54
|
+
await mkdir(vaultDir, { recursive: true });
|
|
55
|
+
await createVaultFile(vaultDir, "note.md", "---\naliases: [MyAlias]\n---\n# Note");
|
|
56
|
+
await createVaultFile(vaultDir, "linker.md", "# Linker\n\n[[MyAlias]]");
|
|
57
|
+
|
|
58
|
+
const result = runNoteOrphanDetect(
|
|
59
|
+
{ flags: { "vault-dir": "vault" } },
|
|
60
|
+
{ workspaceRoot: tempDir },
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const orphanFiles = result.data?.orphans.map((o) => o.file) ?? [];
|
|
64
|
+
expect(orphanFiles).not.toContain("note.md");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("counts self-links correctly (self-link does not count as inbound)", async () => {
|
|
68
|
+
const vaultDir = join(tempDir, "vault");
|
|
69
|
+
await mkdir(vaultDir, { recursive: true });
|
|
70
|
+
await createVaultFile(vaultDir, "self.md", "# Self\n\n[[self]]");
|
|
71
|
+
|
|
72
|
+
const result = runNoteOrphanDetect(
|
|
73
|
+
{ flags: { "vault-dir": "vault" } },
|
|
74
|
+
{ workspaceRoot: tempDir },
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const orphanFiles = result.data?.orphans.map((o) => o.file) ?? [];
|
|
78
|
+
expect(orphanFiles).toContain("vault/self.md");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("always returns exitCode 0 (warnings, not errors)", async () => {
|
|
82
|
+
const vaultDir = join(tempDir, "vault");
|
|
83
|
+
await mkdir(vaultDir, { recursive: true });
|
|
84
|
+
await createVaultFile(vaultDir, "orphan.md", "# Orphan");
|
|
85
|
+
|
|
86
|
+
const result = runNoteOrphanDetect(
|
|
87
|
+
{ flags: { "vault-dir": "vault" } },
|
|
88
|
+
{ workspaceRoot: tempDir },
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect(result.exitCode).toBe(0);
|
|
92
|
+
expect(result.data?.count).toBeGreaterThan(0);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Validate frontmatter consistency across a markdown note vault — checks for required fields in YAML frontmatter.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not import from @warpgogol/* — this module is portable.</item>
|
|
6
|
+
<item>Do not implement domain-specific logic beyond frontmatter validation.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>RFC-0808: initial note.frontmatter.validate command handler.</item>
|
|
11
|
+
</CHANGE_SUMMARY>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { parse as parseYaml } from "yaml";
|
|
17
|
+
import type { ForgeCommandResult } from "../types.ts";
|
|
18
|
+
|
|
19
|
+
interface NoteFrontmatterValidateInput {
|
|
20
|
+
flags: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface FrontmatterViolation {
|
|
24
|
+
file: string;
|
|
25
|
+
field: string;
|
|
26
|
+
rule: "NOTE-02";
|
|
27
|
+
message: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface NoteFrontmatterValidateResult {
|
|
31
|
+
command: "note.frontmatter.validate";
|
|
32
|
+
violations: FrontmatterViolation[];
|
|
33
|
+
count: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function collectMarkdownFiles(dir: string, baseDir: string, results: string[]): void {
|
|
37
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", ".turbo", "dist", ".cache", ".obsidian"]);
|
|
38
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
if (entry.isDirectory() && SKIP_DIRS.has(entry.name)) continue;
|
|
41
|
+
const fullPath = path.join(dir, entry.name);
|
|
42
|
+
const relPath = path.relative(baseDir, fullPath).replace(/\\/g, "/");
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
collectMarkdownFiles(fullPath, baseDir, results);
|
|
45
|
+
} else if (entry.name.endsWith(".md")) {
|
|
46
|
+
results.push(relPath);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function runNoteFrontmatterValidate(
|
|
52
|
+
input: NoteFrontmatterValidateInput,
|
|
53
|
+
context: { workspaceRoot?: string },
|
|
54
|
+
): ForgeCommandResult<NoteFrontmatterValidateResult> {
|
|
55
|
+
const workspaceRoot = context.workspaceRoot ?? process.cwd();
|
|
56
|
+
const vaultDir = (input.flags["vault-dir"] as string) ?? "vault";
|
|
57
|
+
const requiredFields = ((input.flags["fields"] as string) ?? "title")
|
|
58
|
+
.split(",")
|
|
59
|
+
.map((f) => f.trim())
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
|
|
62
|
+
const scanDir = path.join(workspaceRoot, vaultDir);
|
|
63
|
+
|
|
64
|
+
if (!fs.existsSync(scanDir)) {
|
|
65
|
+
return {
|
|
66
|
+
data: {
|
|
67
|
+
command: "note.frontmatter.validate",
|
|
68
|
+
violations: [],
|
|
69
|
+
count: 0,
|
|
70
|
+
},
|
|
71
|
+
exitCode: 0,
|
|
72
|
+
summary: "note.frontmatter.validate: vault directory not found — 0 violations",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const allMdFiles: string[] = [];
|
|
77
|
+
collectMarkdownFiles(scanDir, workspaceRoot, allMdFiles);
|
|
78
|
+
|
|
79
|
+
const violations: FrontmatterViolation[] = [];
|
|
80
|
+
|
|
81
|
+
for (const file of allMdFiles) {
|
|
82
|
+
const fullPath = path.join(workspaceRoot, file);
|
|
83
|
+
try {
|
|
84
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
85
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
86
|
+
if (!fmMatch) {
|
|
87
|
+
for (const field of requiredFields) {
|
|
88
|
+
violations.push({
|
|
89
|
+
file,
|
|
90
|
+
field,
|
|
91
|
+
rule: "NOTE-02",
|
|
92
|
+
message: `File '${file}' has no frontmatter — missing field '${field}'`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
let fm: Record<string, unknown> = {};
|
|
98
|
+
try {
|
|
99
|
+
fm = parseYaml(fmMatch[1]) as Record<string, unknown>;
|
|
100
|
+
} catch {
|
|
101
|
+
for (const field of requiredFields) {
|
|
102
|
+
violations.push({
|
|
103
|
+
file,
|
|
104
|
+
field,
|
|
105
|
+
rule: "NOTE-02",
|
|
106
|
+
message: `File '${file}' has malformed frontmatter — missing field '${field}'`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
for (const field of requiredFields) {
|
|
112
|
+
if (!(field in fm) || fm[field] == null || fm[field] === "") {
|
|
113
|
+
if (field === "title") {
|
|
114
|
+
const hasH1 = /^\s*#\s+\S/.test(content.slice(fmMatch[0].length));
|
|
115
|
+
if (hasH1) continue;
|
|
116
|
+
}
|
|
117
|
+
violations.push({
|
|
118
|
+
file,
|
|
119
|
+
field,
|
|
120
|
+
rule: "NOTE-02",
|
|
121
|
+
message: `File '${file}' missing required field '${field}'`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
// skip unreadable files
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
data: {
|
|
132
|
+
command: "note.frontmatter.validate",
|
|
133
|
+
violations,
|
|
134
|
+
count: violations.length,
|
|
135
|
+
},
|
|
136
|
+
exitCode: violations.length > 0 ? 1 : 0,
|
|
137
|
+
summary:
|
|
138
|
+
violations.length > 0
|
|
139
|
+
? `note.frontmatter.validate: ${violations.length} violation(s) found`
|
|
140
|
+
: "note.frontmatter.validate: OK — 0 violations",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Validate wikilink integrity across a markdown note vault — scans [[wikilinks]] and resolves each against the note graph.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not import from @warpgogol/* — this module is portable.</item>
|
|
6
|
+
<item>Do not implement domain-specific logic beyond wikilink resolution.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>RFC-0808: initial note.link.validate command handler.</item>
|
|
11
|
+
</CHANGE_SUMMARY>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { parse as parseYaml } from "yaml";
|
|
17
|
+
import type { ForgeCommandResult } from "../types.ts";
|
|
18
|
+
|
|
19
|
+
interface NoteLinkValidateInput {
|
|
20
|
+
flags: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface NoteLinkViolation {
|
|
24
|
+
file: string;
|
|
25
|
+
line: number;
|
|
26
|
+
link: string;
|
|
27
|
+
rule: "NOTE-01";
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface NoteLinkValidateResult {
|
|
32
|
+
command: "note.link.validate";
|
|
33
|
+
violations: NoteLinkViolation[];
|
|
34
|
+
count: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function collectMarkdownFiles(dir: string, baseDir: string, results: string[]): void {
|
|
38
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", ".turbo", "dist", ".cache", ".obsidian"]);
|
|
39
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
if (entry.isDirectory() && SKIP_DIRS.has(entry.name)) continue;
|
|
42
|
+
const fullPath = path.join(dir, entry.name);
|
|
43
|
+
const relPath = path.relative(baseDir, fullPath).replace(/\\/g, "/");
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
collectMarkdownFiles(fullPath, baseDir, results);
|
|
46
|
+
} else if (entry.name.endsWith(".md")) {
|
|
47
|
+
results.push(relPath);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function runNoteLinkValidate(
|
|
53
|
+
input: NoteLinkValidateInput,
|
|
54
|
+
context: { workspaceRoot?: string },
|
|
55
|
+
): ForgeCommandResult<NoteLinkValidateResult> {
|
|
56
|
+
const workspaceRoot = context.workspaceRoot ?? process.cwd();
|
|
57
|
+
const vaultDir = (input.flags["vault-dir"] as string) ?? "vault";
|
|
58
|
+
const pathFilter = input.flags["path"] as string | undefined;
|
|
59
|
+
const scanDir = pathFilter
|
|
60
|
+
? path.join(workspaceRoot, vaultDir, pathFilter)
|
|
61
|
+
: path.join(workspaceRoot, vaultDir);
|
|
62
|
+
|
|
63
|
+
if (!fs.existsSync(scanDir)) {
|
|
64
|
+
return {
|
|
65
|
+
data: {
|
|
66
|
+
command: "note.link.validate",
|
|
67
|
+
violations: [],
|
|
68
|
+
count: 0,
|
|
69
|
+
},
|
|
70
|
+
exitCode: 0,
|
|
71
|
+
summary: "note.link.validate: vault directory not found — 0 violations",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const allMdFiles: string[] = [];
|
|
76
|
+
collectMarkdownFiles(scanDir, workspaceRoot, allMdFiles);
|
|
77
|
+
|
|
78
|
+
const noteMap = new Map<string, string>();
|
|
79
|
+
for (const mdFile of allMdFiles) {
|
|
80
|
+
const basename = path.basename(mdFile, ".md");
|
|
81
|
+
noteMap.set(basename, mdFile);
|
|
82
|
+
const fullPath = path.join(workspaceRoot, mdFile);
|
|
83
|
+
try {
|
|
84
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
85
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
86
|
+
if (fmMatch) {
|
|
87
|
+
try {
|
|
88
|
+
const fm = parseYaml(fmMatch[1]) as Record<string, unknown>;
|
|
89
|
+
if (fm.aliases && Array.isArray(fm.aliases)) {
|
|
90
|
+
for (const alias of fm.aliases) {
|
|
91
|
+
if (typeof alias === "string") noteMap.set(alias, mdFile);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// skip malformed frontmatter
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// skip unreadable files
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const linkRegex = /\[\[([^\]]+?)\]\]/g;
|
|
104
|
+
const violations: NoteLinkViolation[] = [];
|
|
105
|
+
|
|
106
|
+
for (const file of allMdFiles) {
|
|
107
|
+
const fullPath = path.join(workspaceRoot, file);
|
|
108
|
+
try {
|
|
109
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
110
|
+
const lines = content.split(/\r?\n/);
|
|
111
|
+
for (let i = 0; i < lines.length; i++) {
|
|
112
|
+
let match: RegExpExecArray | null;
|
|
113
|
+
linkRegex.lastIndex = 0;
|
|
114
|
+
while ((match = linkRegex.exec(lines[i])) !== null) {
|
|
115
|
+
const linkTarget = match[1].split("|")[0].split("#")[0].trim();
|
|
116
|
+
if (!linkTarget) continue;
|
|
117
|
+
if (!noteMap.has(linkTarget)) {
|
|
118
|
+
violations.push({
|
|
119
|
+
file,
|
|
120
|
+
line: i + 1,
|
|
121
|
+
link: match[0],
|
|
122
|
+
rule: "NOTE-01",
|
|
123
|
+
message: `Wikilink target '${linkTarget}' not found in vault`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
// skip unreadable files
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
data: {
|
|
135
|
+
command: "note.link.validate",
|
|
136
|
+
violations,
|
|
137
|
+
count: violations.length,
|
|
138
|
+
},
|
|
139
|
+
exitCode: violations.length > 0 ? 1 : 0,
|
|
140
|
+
summary:
|
|
141
|
+
violations.length > 0
|
|
142
|
+
? `note.link.validate: ${violations.length} violation(s) found`
|
|
143
|
+
: "note.link.validate: OK — 0 violations",
|
|
144
|
+
};
|
|
145
|
+
}
|