@warpgogol/forge 2.8.0 → 2.8.1
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/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/forge-config.ts +524 -0
- package/src/forge-module.ts +39 -0
- package/src/index.ts +172 -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/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/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/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 +51 -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 +217 -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/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/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,156 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Cross-skill duplicate detection and promotion planning for shared knowledge layer (RFC-0663).</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not read or write files — that is handled by parse.ts/serialize.ts and the distill skill.</item>
|
|
6
|
+
<item>Do not execute promotions — that is the operator's decision inside fo-knowledge-distill.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>RFC-0663: initial cross-skill duplicate detection and promotion planning.</item>
|
|
11
|
+
</CHANGE_SUMMARY>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ParsedKnowledgeFile, KnowledgeEntry, KnowledgeEntryMeta } from "./schema.ts";
|
|
15
|
+
|
|
16
|
+
const STOP_WORDS = new Set(["the", "a", "an"]);
|
|
17
|
+
|
|
18
|
+
const MIN_CONTAINMENT_LENGTH = 20;
|
|
19
|
+
const MIN_CONTAINMENT_RATIO = 0.6;
|
|
20
|
+
|
|
21
|
+
export interface DuplicatePair {
|
|
22
|
+
a: { skill: string; entryId: string; title: string };
|
|
23
|
+
b: { skill: string; entryId: string; title: string };
|
|
24
|
+
normalizedTitle: string;
|
|
25
|
+
kind: "exact" | "containment";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PromotionPlan {
|
|
29
|
+
sharedEntry: KnowledgeEntry;
|
|
30
|
+
localPointers: Array<{ skill: string; file: string; entryId: string }>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function normalizeTitle(title: string): string {
|
|
34
|
+
let result = title.toLowerCase();
|
|
35
|
+
result = result.replace(/[^\p{L}\p{N}\s]/gu, " ");
|
|
36
|
+
result = result.replace(/\s+/g, " ").trim();
|
|
37
|
+
const words = result.split(" ").filter((w) => w.length > 0 && !STOP_WORDS.has(w));
|
|
38
|
+
return words.join(" ");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isExcludedEntry(meta: KnowledgeEntryMeta): boolean {
|
|
42
|
+
if (meta.status === "stale" || meta.status === "archived") return true;
|
|
43
|
+
if (meta.promotedTo !== undefined && meta.promotedTo !== null) return true;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isLinkedPair(metaA: KnowledgeEntryMeta, metaB: KnowledgeEntryMeta): boolean {
|
|
48
|
+
if (metaA.supersedes && metaB.id && metaA.supersedes.includes(metaB.id)) return true;
|
|
49
|
+
if (metaB.supersedes && metaA.id && metaB.supersedes.includes(metaA.id)) return true;
|
|
50
|
+
if (metaA.promotedTo && metaB.id && metaA.promotedTo === `shared/${metaB.id}`) return true;
|
|
51
|
+
if (metaB.promotedTo && metaA.id && metaB.promotedTo === `shared/${metaA.id}`) return true;
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface ActiveEntry {
|
|
56
|
+
skill: string;
|
|
57
|
+
entry: KnowledgeEntry;
|
|
58
|
+
normalizedTitle: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function collectActiveEntries(
|
|
62
|
+
files: Array<{ skill: string; parsed: ParsedKnowledgeFile }>,
|
|
63
|
+
): ActiveEntry[] {
|
|
64
|
+
const active: ActiveEntry[] = [];
|
|
65
|
+
for (const file of files) {
|
|
66
|
+
if (file.parsed.isKnowledgeAdjacent) continue;
|
|
67
|
+
if (file.parsed.parseIssues.length > 0) continue;
|
|
68
|
+
for (const entry of file.parsed.entries) {
|
|
69
|
+
if (isExcludedEntry(entry.meta)) continue;
|
|
70
|
+
const normalized = normalizeTitle(entry.title);
|
|
71
|
+
if (normalized.length === 0) continue;
|
|
72
|
+
active.push({ skill: file.skill, entry, normalizedTitle: normalized });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return active;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function detectDuplicatePrinciples(
|
|
79
|
+
files: Array<{ skill: string; parsed: ParsedKnowledgeFile }>,
|
|
80
|
+
): DuplicatePair[] {
|
|
81
|
+
const active = collectActiveEntries(files);
|
|
82
|
+
const pairs: DuplicatePair[] = [];
|
|
83
|
+
|
|
84
|
+
for (let i = 0; i < active.length; i++) {
|
|
85
|
+
for (let j = i + 1; j < active.length; j++) {
|
|
86
|
+
const a = active[i];
|
|
87
|
+
const b = active[j];
|
|
88
|
+
|
|
89
|
+
if (isLinkedPair(a.entry.meta, b.entry.meta)) continue;
|
|
90
|
+
|
|
91
|
+
if (a.normalizedTitle === b.normalizedTitle) {
|
|
92
|
+
pairs.push({
|
|
93
|
+
a: { skill: a.skill, entryId: a.entry.meta.id, title: a.entry.title },
|
|
94
|
+
b: { skill: b.skill, entryId: b.entry.meta.id, title: b.entry.title },
|
|
95
|
+
normalizedTitle: a.normalizedTitle,
|
|
96
|
+
kind: "exact",
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const shorter = a.normalizedTitle.length <= b.normalizedTitle.length ? a : b;
|
|
102
|
+
const longer = a.normalizedTitle.length <= b.normalizedTitle.length ? b : a;
|
|
103
|
+
|
|
104
|
+
if (shorter.normalizedTitle.length < MIN_CONTAINMENT_LENGTH) continue;
|
|
105
|
+
if (shorter.normalizedTitle.length < longer.normalizedTitle.length * MIN_CONTAINMENT_RATIO)
|
|
106
|
+
continue;
|
|
107
|
+
|
|
108
|
+
if (longer.normalizedTitle.includes(shorter.normalizedTitle)) {
|
|
109
|
+
pairs.push({
|
|
110
|
+
a: { skill: a.skill, entryId: a.entry.meta.id, title: a.entry.title },
|
|
111
|
+
b: { skill: b.skill, entryId: b.entry.meta.id, title: b.entry.title },
|
|
112
|
+
normalizedTitle: shorter.normalizedTitle,
|
|
113
|
+
kind: "containment",
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return pairs;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function planPromotion(
|
|
123
|
+
sources: Array<{ skill: string; file: string; entry: KnowledgeEntry }>,
|
|
124
|
+
merged: { title: string; body: string },
|
|
125
|
+
nextSharedId: string,
|
|
126
|
+
today: string,
|
|
127
|
+
): PromotionPlan {
|
|
128
|
+
const totalConfirmations = sources.reduce((sum, s) => sum + (s.entry.meta.confirmations ?? 0), 0);
|
|
129
|
+
|
|
130
|
+
const promotedFrom = sources.map((s) => `${s.skill}/${s.entry.meta.id}`);
|
|
131
|
+
|
|
132
|
+
const sharedMeta: KnowledgeEntryMeta = {
|
|
133
|
+
id: nextSharedId,
|
|
134
|
+
layer: "L2",
|
|
135
|
+
created: today,
|
|
136
|
+
lastConfirmedAt: today,
|
|
137
|
+
confirmations: totalConfirmations,
|
|
138
|
+
status: "active",
|
|
139
|
+
promotedFrom,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const sharedEntry: KnowledgeEntry = {
|
|
143
|
+
meta: sharedMeta,
|
|
144
|
+
title: merged.title,
|
|
145
|
+
body: merged.body,
|
|
146
|
+
lineStart: 0,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const localPointers = sources.map((s) => ({
|
|
150
|
+
skill: s.skill,
|
|
151
|
+
file: s.file,
|
|
152
|
+
entryId: s.entry.meta.id,
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
return { sharedEntry, localPointers };
|
|
156
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Define the Zod metadata schema for knowledge entries with layer-specific refinements (RFC-0660).</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not parse markdown — that is handled by parse.ts.</item>
|
|
6
|
+
<item>Do not serialize — that is handled by serialize.ts.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>RFC-0660: initial knowledge entry metadata schema with L0/L1/L2 layer-specific refinements.</item>
|
|
11
|
+
<item>RFC-0663: added promotedFrom field for shared-layer provenance tracking.</item>
|
|
12
|
+
</CHANGE_SUMMARY>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
|
|
17
|
+
export type KnowledgeLayer = "L0" | "L1" | "L2";
|
|
18
|
+
export type KnowledgeEntryStatus = "active" | "stale" | "superseded" | "archived";
|
|
19
|
+
|
|
20
|
+
const ENTRY_ID_PATTERN = /^K-\d{4}$/;
|
|
21
|
+
const PROMOTED_TO_PATTERN = /^shared\/K-\d{4}$/;
|
|
22
|
+
const PROMOTED_FROM_PATTERN = /^[a-z0-9-]+\/K-\d{4}$/;
|
|
23
|
+
|
|
24
|
+
const baseMetaSchema = z.object({
|
|
25
|
+
id: z.string().regex(ENTRY_ID_PATTERN, "id must match ^K-\\d{4}$"),
|
|
26
|
+
layer: z.enum(["L0", "L1", "L2"]),
|
|
27
|
+
created: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "created must be YYYY-MM-DD"),
|
|
28
|
+
lastConfirmedAt: z.union([z.string().regex(/^\d{4}-\d{2}-\d{2}$/), z.null()]).optional(),
|
|
29
|
+
confirmations: z.number().int().min(0).optional(),
|
|
30
|
+
expiresAt: z.union([z.string().regex(/^\d{4}-\d{2}-\d{2}$/), z.null()]).optional(),
|
|
31
|
+
supersedes: z.array(z.string().regex(ENTRY_ID_PATTERN)).optional(),
|
|
32
|
+
promotedTo: z.union([z.string().regex(PROMOTED_TO_PATTERN), z.null()]).optional(),
|
|
33
|
+
promotedFrom: z.array(z.string().regex(PROMOTED_FROM_PATTERN)).optional(),
|
|
34
|
+
status: z.enum(["active", "stale", "superseded", "archived"]),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const knowledgeEntryMetaSchema = baseMetaSchema.superRefine((data, ctx) => {
|
|
38
|
+
if (data.layer === "L0" || data.layer === "L1") {
|
|
39
|
+
if (data.confirmations !== undefined) {
|
|
40
|
+
ctx.addIssue({
|
|
41
|
+
code: z.ZodIssueCode.custom,
|
|
42
|
+
message: `confirmations is forbidden on ${data.layer} entries`,
|
|
43
|
+
path: ["confirmations"],
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (data.lastConfirmedAt !== undefined) {
|
|
47
|
+
ctx.addIssue({
|
|
48
|
+
code: z.ZodIssueCode.custom,
|
|
49
|
+
message: `lastConfirmedAt is forbidden on ${data.layer} entries`,
|
|
50
|
+
path: ["lastConfirmedAt"],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (data.layer === "L2") {
|
|
56
|
+
if (data.confirmations === undefined) {
|
|
57
|
+
ctx.addIssue({
|
|
58
|
+
code: z.ZodIssueCode.custom,
|
|
59
|
+
message: "confirmations is required on L2 entries",
|
|
60
|
+
path: ["confirmations"],
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (data.lastConfirmedAt === undefined) {
|
|
64
|
+
ctx.addIssue({
|
|
65
|
+
code: z.ZodIssueCode.custom,
|
|
66
|
+
message: "lastConfirmedAt is required on L2 entries",
|
|
67
|
+
path: ["lastConfirmedAt"],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export interface KnowledgeEntryMeta {
|
|
74
|
+
id: string;
|
|
75
|
+
layer: KnowledgeLayer;
|
|
76
|
+
created: string;
|
|
77
|
+
lastConfirmedAt?: string | null;
|
|
78
|
+
confirmations?: number;
|
|
79
|
+
expiresAt?: string | null;
|
|
80
|
+
supersedes?: string[];
|
|
81
|
+
promotedTo?: string | null;
|
|
82
|
+
promotedFrom?: string[];
|
|
83
|
+
status: KnowledgeEntryStatus;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface KnowledgeEntry {
|
|
87
|
+
meta: KnowledgeEntryMeta;
|
|
88
|
+
title: string;
|
|
89
|
+
body: string;
|
|
90
|
+
lineStart: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface LegacySection {
|
|
94
|
+
text: string;
|
|
95
|
+
lineStart: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ParseIssue {
|
|
99
|
+
line: number;
|
|
100
|
+
message: string;
|
|
101
|
+
entryId?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ParsedKnowledgeFile {
|
|
105
|
+
path: string;
|
|
106
|
+
layer: KnowledgeLayer | null;
|
|
107
|
+
preamble: string;
|
|
108
|
+
entries: KnowledgeEntry[];
|
|
109
|
+
legacySections: LegacySection[];
|
|
110
|
+
parseIssues: ParseIssue[];
|
|
111
|
+
isKnowledgeAdjacent: boolean;
|
|
112
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Round-trip serializer for knowledge files — reconstructs markdown from ParsedKnowledgeFile (RFC-0660).</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not parse — that is handled by parse.ts.</item>
|
|
6
|
+
</non-goals>
|
|
7
|
+
</MODULE_CONTRACT>
|
|
8
|
+
<CHANGE_SUMMARY>
|
|
9
|
+
<item>RFC-0660: initial round-trip serializer for structured knowledge entries.</item>
|
|
10
|
+
<item>RFC-0663: added promotedFrom to FIELD_ORDER for shared-layer provenance.</item>
|
|
11
|
+
</CHANGE_SUMMARY>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ParsedKnowledgeFile, KnowledgeEntryMeta } from "./schema.ts";
|
|
15
|
+
|
|
16
|
+
const FIELD_ORDER: (keyof KnowledgeEntryMeta)[] = [
|
|
17
|
+
"id",
|
|
18
|
+
"layer",
|
|
19
|
+
"created",
|
|
20
|
+
"lastConfirmedAt",
|
|
21
|
+
"confirmations",
|
|
22
|
+
"expiresAt",
|
|
23
|
+
"supersedes",
|
|
24
|
+
"promotedTo",
|
|
25
|
+
"promotedFrom",
|
|
26
|
+
"status",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function formatYamlValue(value: unknown): string {
|
|
30
|
+
if (value === null) return "null";
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
if (value.length === 0) return "[]";
|
|
33
|
+
return `[${value.join(", ")}]`;
|
|
34
|
+
}
|
|
35
|
+
if (typeof value === "string") return value;
|
|
36
|
+
return String(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function serializeMeta(meta: KnowledgeEntryMeta): string {
|
|
40
|
+
const lines: string[] = ["```knowledge-entry"];
|
|
41
|
+
for (const field of FIELD_ORDER) {
|
|
42
|
+
const value = meta[field];
|
|
43
|
+
if (value === undefined) continue;
|
|
44
|
+
lines.push(`${field}: ${formatYamlValue(value)}`);
|
|
45
|
+
}
|
|
46
|
+
lines.push("```");
|
|
47
|
+
return lines.join("\n");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function serializeKnowledgeFile(parsed: ParsedKnowledgeFile): string {
|
|
51
|
+
const parts: string[] = [];
|
|
52
|
+
|
|
53
|
+
if (parsed.preamble) {
|
|
54
|
+
parts.push(parsed.preamble);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const entry of parsed.entries) {
|
|
58
|
+
parts.push(`### ${entry.meta.id}: ${entry.title}`);
|
|
59
|
+
parts.push("");
|
|
60
|
+
parts.push(serializeMeta(entry.meta));
|
|
61
|
+
if (entry.body) {
|
|
62
|
+
parts.push("");
|
|
63
|
+
parts.push(entry.body);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const legacy of parsed.legacySections) {
|
|
68
|
+
parts.push(legacy.text);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return parts.join("\n").replace(/\n{3,}/g, "\n\n") + "\n";
|
|
72
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Shared git utilities for migration adapters — git init and git history transfer via format-patch + git am (RFC-0547).</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not import from @warpgogol/* — this module is portable.</item>
|
|
6
|
+
<item>Do not implement adapter-specific logic — only shared git operations.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>RFC-0547: extract shared postSetup git logic from duplicated adapter implementations.</item>
|
|
11
|
+
<item>Replace fs.rmSync with trashSync for patch directory cleanup (trash bin for LLM-initiated deletions).</item>
|
|
12
|
+
</CHANGE_SUMMARY>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { execFileSync } from "node:child_process";
|
|
18
|
+
import { trashSync } from "../utils/fs-trash-sync.ts";
|
|
19
|
+
import type { AdapterAnalysis } from "./types.ts";
|
|
20
|
+
|
|
21
|
+
export function runPostSetup(
|
|
22
|
+
sourceDir: string,
|
|
23
|
+
targetDir: string,
|
|
24
|
+
analysis: AdapterAnalysis,
|
|
25
|
+
): void {
|
|
26
|
+
const gitDir = path.join(targetDir, ".git");
|
|
27
|
+
|
|
28
|
+
if (analysis.gitHistory) {
|
|
29
|
+
try {
|
|
30
|
+
const patchDir = path.join(targetDir, ".forge-migration-patches");
|
|
31
|
+
fs.mkdirSync(patchDir, { recursive: true });
|
|
32
|
+
execFileSync("git", ["-C", sourceDir, "format-patch", "--all", "-o", patchDir], {
|
|
33
|
+
stdio: "pipe",
|
|
34
|
+
});
|
|
35
|
+
execFileSync("git", ["init"], { cwd: targetDir, stdio: "pipe" });
|
|
36
|
+
execFileSync("git", ["config", "user.email", "forge@warpgogol.dev"], {
|
|
37
|
+
cwd: targetDir,
|
|
38
|
+
stdio: "pipe",
|
|
39
|
+
});
|
|
40
|
+
execFileSync("git", ["config", "user.name", "Forge Migration"], {
|
|
41
|
+
cwd: targetDir,
|
|
42
|
+
stdio: "pipe",
|
|
43
|
+
});
|
|
44
|
+
const patches = fs
|
|
45
|
+
.readdirSync(patchDir)
|
|
46
|
+
.filter((f) => f.endsWith(".patch"))
|
|
47
|
+
.sort();
|
|
48
|
+
if (patches.length > 0) {
|
|
49
|
+
for (const patch of patches) {
|
|
50
|
+
execFileSync("git", ["am", path.join(patchDir, patch)], {
|
|
51
|
+
cwd: targetDir,
|
|
52
|
+
stdio: "pipe",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
trashSync(patchDir);
|
|
57
|
+
return;
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.warn(
|
|
60
|
+
`forge: git history transfer failed, falling back to clean git init: ${err instanceof Error ? err.message : String(err)}`,
|
|
61
|
+
);
|
|
62
|
+
if (!fs.existsSync(gitDir)) {
|
|
63
|
+
execFileSync("git", ["init"], { cwd: targetDir, stdio: "pipe" });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
if (!fs.existsSync(gitDir)) {
|
|
68
|
+
execFileSync("git", ["init"], { cwd: targetDir, stdio: "pipe" });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Discover and categorize git-ignored/untracked files in a source project for interactive transfer selection during onboarding.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not import from @warpgogol/* — this module is portable.</item>
|
|
6
|
+
<item>Do not copy files — discovery and categorization only.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>Initial: discoverIgnoredFiles function that scans source for git-ignored files, categorizes them by type (config, data, runtime-state, cache, other), and returns sized categories for operator selection.</item>
|
|
11
|
+
</CHANGE_SUMMARY>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
17
|
+
|
|
18
|
+
export type IgnoredFileCategoryId = "config" | "data" | "runtime-state" | "cache" | "other";
|
|
19
|
+
|
|
20
|
+
export interface IgnoredFileCategory {
|
|
21
|
+
id: IgnoredFileCategoryId;
|
|
22
|
+
label: string;
|
|
23
|
+
description: string;
|
|
24
|
+
paths: string[];
|
|
25
|
+
totalSizeBytes: number;
|
|
26
|
+
fileCount: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface CategoryDef {
|
|
30
|
+
id: IgnoredFileCategoryId;
|
|
31
|
+
label: string;
|
|
32
|
+
description: string;
|
|
33
|
+
matchers: ((relPath: string) => boolean)[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const CONFIG_MATCHERS: ((p: string) => boolean)[] = [
|
|
37
|
+
(p) => p === ".env" || p.startsWith(".env."),
|
|
38
|
+
(p) => p.endsWith(".key") || p.endsWith(".pem") || p.endsWith(".crt"),
|
|
39
|
+
(p) => p === ".secret" || p.startsWith("secret"),
|
|
40
|
+
(p) => p.startsWith("secrets/"),
|
|
41
|
+
(p) => p.includes("/.env") || p.includes("/.env."),
|
|
42
|
+
(p) => p === ".envrc" || p.startsWith(".envrc."),
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const DATA_MATCHERS: ((p: string) => boolean)[] = [
|
|
46
|
+
(p) => p.startsWith(".input/") || p.includes("/.input/"),
|
|
47
|
+
(p) => p.startsWith("input/") || p.includes("/input/"),
|
|
48
|
+
(p) => p.startsWith("inputs/") || p.includes("/inputs/"),
|
|
49
|
+
(p) => p.startsWith("data/") || p.includes("/data/"),
|
|
50
|
+
(p) => p.startsWith("assets/") || p.includes("/assets/"),
|
|
51
|
+
(p) => p.startsWith("batches/") || p.includes("/batches/"),
|
|
52
|
+
(p) => p.endsWith(".db") || p.endsWith(".sqlite") || p.endsWith(".sqlite3"),
|
|
53
|
+
(p) => p.endsWith(".ndjson") && !p.startsWith("bordbuch/"),
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const RUNTIME_STATE_MATCHERS: ((p: string) => boolean)[] = [
|
|
57
|
+
(p) => p.startsWith("storage/") || p.includes("/storage/"),
|
|
58
|
+
(p) => p.startsWith(".state/") || p.includes("/.state/"),
|
|
59
|
+
(p) => p.startsWith("state/") || p.includes("/state/"),
|
|
60
|
+
(p) => p.endsWith(".log"),
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const CACHE_MATCHERS: ((p: string) => boolean)[] = [
|
|
64
|
+
(p) => p.startsWith(".cache/") || p.includes("/.cache/"),
|
|
65
|
+
(p) => p.startsWith("cache/") || p.includes("/cache/"),
|
|
66
|
+
(p) => p.includes(".playwright") || p.includes("browser-profile"),
|
|
67
|
+
(p) => p.endsWith(".tmp") || p.startsWith(".tmp"),
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const CATEGORY_DEFS: CategoryDef[] = [
|
|
71
|
+
{
|
|
72
|
+
id: "config",
|
|
73
|
+
label: "Configuration & secrets",
|
|
74
|
+
description:
|
|
75
|
+
"Environment files, API keys, certificates, and local configuration needed for the project to run.",
|
|
76
|
+
matchers: CONFIG_MATCHERS,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: "data",
|
|
80
|
+
label: "Data & inputs",
|
|
81
|
+
description: "Input data, datasets, databases, and batch files used by the project.",
|
|
82
|
+
matchers: DATA_MATCHERS,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "runtime-state",
|
|
86
|
+
label: "Runtime state",
|
|
87
|
+
description: "Crawlee state, logs, and other runtime artifacts from previous executions.",
|
|
88
|
+
matchers: RUNTIME_STATE_MATCHERS,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "cache",
|
|
92
|
+
label: "Caches",
|
|
93
|
+
description: "Browser profiles, temporary files, and build caches that can be regenerated.",
|
|
94
|
+
matchers: CACHE_MATCHERS,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "other",
|
|
98
|
+
label: "Other ignored files",
|
|
99
|
+
description: "Files matched by .gitignore that don't fit into any standard category.",
|
|
100
|
+
matchers: [],
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const CATEGORY_PRIORITY: Record<IgnoredFileCategoryId, number> = {
|
|
105
|
+
config: 0,
|
|
106
|
+
data: 1,
|
|
107
|
+
"runtime-state": 2,
|
|
108
|
+
cache: 3,
|
|
109
|
+
other: 4,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
function categorizePath(relPath: string): CategoryDef {
|
|
113
|
+
const normalized = relPath.replace(/\\/g, "/");
|
|
114
|
+
for (const def of CATEGORY_DEFS) {
|
|
115
|
+
if (def.matchers.some((m) => m(normalized))) {
|
|
116
|
+
return def;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return CATEGORY_DEFS[CATEGORY_DEFS.length - 1];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getDirStats(dirPath: string): { size: number; count: number } {
|
|
123
|
+
let size = 0;
|
|
124
|
+
let count = 0;
|
|
125
|
+
try {
|
|
126
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
129
|
+
if (entry.isDirectory()) {
|
|
130
|
+
const sub = getDirStats(fullPath);
|
|
131
|
+
size += sub.size;
|
|
132
|
+
count += sub.count;
|
|
133
|
+
} else if (entry.isFile()) {
|
|
134
|
+
try {
|
|
135
|
+
size += fs.statSync(fullPath).size;
|
|
136
|
+
count++;
|
|
137
|
+
} catch {
|
|
138
|
+
// File may have been removed
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// Directory may have been removed
|
|
144
|
+
}
|
|
145
|
+
return { size, count };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getPathSize(fullPath: string): { sizeBytes: number; fileCount: number } {
|
|
149
|
+
try {
|
|
150
|
+
const stat = fs.statSync(fullPath);
|
|
151
|
+
if (stat.isDirectory()) {
|
|
152
|
+
const { size, count } = getDirStats(fullPath);
|
|
153
|
+
return { sizeBytes: size, fileCount: count };
|
|
154
|
+
}
|
|
155
|
+
return { sizeBytes: stat.size, fileCount: 1 };
|
|
156
|
+
} catch {
|
|
157
|
+
return { sizeBytes: 0, fileCount: 0 };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function discoverIgnoredFiles(sourceDir: string): IgnoredFileCategory[] {
|
|
162
|
+
const hasGit = fs.existsSync(path.join(sourceDir, ".git"));
|
|
163
|
+
let ignoredPaths: string[] = [];
|
|
164
|
+
|
|
165
|
+
if (hasGit) {
|
|
166
|
+
try {
|
|
167
|
+
const output = execFileSync("git", ["-C", sourceDir, "status", "--ignored", "--porcelain"], {
|
|
168
|
+
encoding: "utf-8",
|
|
169
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
170
|
+
timeout: 30_000,
|
|
171
|
+
});
|
|
172
|
+
ignoredPaths = output
|
|
173
|
+
.split("\n")
|
|
174
|
+
.filter((l) => l.startsWith("!! "))
|
|
175
|
+
.map((l) => l.slice(3).trim())
|
|
176
|
+
.filter((p) => p.length > 0);
|
|
177
|
+
} catch {
|
|
178
|
+
// git command failed — return empty
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (ignoredPaths.length === 0) {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const categoryMap = new Map<IgnoredFileCategoryId, IgnoredFileCategory>();
|
|
188
|
+
|
|
189
|
+
for (const relPath of ignoredPaths) {
|
|
190
|
+
const fullPath = path.join(sourceDir, relPath);
|
|
191
|
+
const def = categorizePath(relPath);
|
|
192
|
+
const { sizeBytes, fileCount } = getPathSize(fullPath);
|
|
193
|
+
|
|
194
|
+
if (!categoryMap.has(def.id)) {
|
|
195
|
+
categoryMap.set(def.id, {
|
|
196
|
+
id: def.id,
|
|
197
|
+
label: def.label,
|
|
198
|
+
description: def.description,
|
|
199
|
+
paths: [],
|
|
200
|
+
totalSizeBytes: 0,
|
|
201
|
+
fileCount: 0,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const cat = categoryMap.get(def.id)!;
|
|
206
|
+
cat.paths.push(relPath);
|
|
207
|
+
cat.totalSizeBytes += sizeBytes;
|
|
208
|
+
cat.fileCount += fileCount;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return Array.from(categoryMap.values()).sort(
|
|
212
|
+
(a, b) => CATEGORY_PRIORITY[a.id] - CATEGORY_PRIORITY[b.id],
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function formatSize(bytes: number): string {
|
|
217
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
218
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
219
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
220
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
221
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Barrel export for migration-adapter module (RFC-0546).</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not re-export implementation details — only public types and registry functions.</item>
|
|
6
|
+
</non-goals>
|
|
7
|
+
</MODULE_CONTRACT>
|
|
8
|
+
<CHANGE_SUMMARY>
|
|
9
|
+
<item>RFC-0546: initial barrel export for migration-adapters.</item>
|
|
10
|
+
</CHANGE_SUMMARY>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type { MigrationAdapter, AdapterAnalysis, MigrationResult, Conflict } from "./types.ts";
|
|
14
|
+
|
|
15
|
+
export { FORGE_PROTECTED_PATHS, DEFAULT_EXCLUDE_PATTERNS } from "./types.ts";
|
|
16
|
+
|
|
17
|
+
export { nodeTypescriptPnpmAdapter } from "./node-typescript-pnpm/index.ts";
|
|
18
|
+
export { phaserPnpmAdapter } from "./phaser-pnpm/index.ts";
|
|
19
|
+
export { getAdapters, detectAdapter, detectAdapters } from "./registry.ts";
|
|
20
|
+
export { discoverIgnoredFiles, formatSize } from "./ignored-files.ts";
|
|
21
|
+
export type { IgnoredFileCategory, IgnoredFileCategoryId } from "./ignored-files.ts";
|