@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,227 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Tolerant markdown parser for knowledge files — produces ParsedKnowledgeFile with entries, legacy sections, and parse issues (RFC-0660).</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not validate entry uniqueness or cross-references — that is handled by SKILL-20 in skill-validate.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 tolerant parser for structured knowledge entries.</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 {
|
|
18
|
+
knowledgeEntryMetaSchema,
|
|
19
|
+
type KnowledgeLayer,
|
|
20
|
+
type KnowledgeEntry,
|
|
21
|
+
type KnowledgeEntryMeta,
|
|
22
|
+
type LegacySection,
|
|
23
|
+
type ParseIssue,
|
|
24
|
+
type ParsedKnowledgeFile,
|
|
25
|
+
} from "./schema.ts";
|
|
26
|
+
|
|
27
|
+
const ENTRY_HEADING_PATTERN = /^###\s+(K-\d{4})\s*:\s*(.+)$/;
|
|
28
|
+
const KNOWLEDGE_ENTRY_FENCE = "knowledge-entry";
|
|
29
|
+
const LAYER_COMMENT_PATTERN = /<!--\s*knowledge-layer:\s*(L[012])\s*-->/;
|
|
30
|
+
|
|
31
|
+
const FILENAME_LAYER_MAP: Record<string, KnowledgeLayer> = {
|
|
32
|
+
"qa-log.md": "L0",
|
|
33
|
+
"fix-patterns.md": "L1",
|
|
34
|
+
"learned-principles.md": "L2",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function detectLayer(content: string, fileName: string): KnowledgeLayer | null {
|
|
38
|
+
const commentMatch = content.match(LAYER_COMMENT_PATTERN);
|
|
39
|
+
if (commentMatch) {
|
|
40
|
+
return commentMatch[1] as KnowledgeLayer;
|
|
41
|
+
}
|
|
42
|
+
return FILENAME_LAYER_MAP[fileName] ?? null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function parseKnowledgeFile(filePath: string): ParsedKnowledgeFile {
|
|
46
|
+
let content: string;
|
|
47
|
+
try {
|
|
48
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
49
|
+
} catch {
|
|
50
|
+
// File read errors (EISDIR, ENOENT, etc.) — return empty result
|
|
51
|
+
return {
|
|
52
|
+
path: filePath,
|
|
53
|
+
layer: null,
|
|
54
|
+
preamble: "",
|
|
55
|
+
entries: [],
|
|
56
|
+
legacySections: [],
|
|
57
|
+
parseIssues: [],
|
|
58
|
+
isKnowledgeAdjacent: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const fileName = path.basename(filePath);
|
|
62
|
+
const lines = content.split(/\r?\n/);
|
|
63
|
+
const layer = detectLayer(content, fileName);
|
|
64
|
+
|
|
65
|
+
const entries: KnowledgeEntry[] = [];
|
|
66
|
+
const legacySections: LegacySection[] = [];
|
|
67
|
+
const parseIssues: ParseIssue[] = [];
|
|
68
|
+
|
|
69
|
+
let i = 0;
|
|
70
|
+
let preambleEnd = 0;
|
|
71
|
+
|
|
72
|
+
// Find the first entry heading or the first non-preamble content
|
|
73
|
+
let firstEntryLine = -1;
|
|
74
|
+
for (let j = 0; j < lines.length; j++) {
|
|
75
|
+
if (ENTRY_HEADING_PATTERN.test(lines[j])) {
|
|
76
|
+
firstEntryLine = j;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// If no entry headings and no layer declaration, it's a knowledge-adjacent file
|
|
82
|
+
const isKnowledgeAdjacent = firstEntryLine === -1 && layer === null;
|
|
83
|
+
|
|
84
|
+
if (isKnowledgeAdjacent) {
|
|
85
|
+
return {
|
|
86
|
+
path: filePath,
|
|
87
|
+
layer: null,
|
|
88
|
+
preamble: content,
|
|
89
|
+
entries: [],
|
|
90
|
+
legacySections: [],
|
|
91
|
+
parseIssues: [],
|
|
92
|
+
isKnowledgeAdjacent: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Preamble is everything before the first entry heading
|
|
97
|
+
if (firstEntryLine === -1) {
|
|
98
|
+
// Has layer but no entries — all content is preamble (structured-empty file)
|
|
99
|
+
return {
|
|
100
|
+
path: filePath,
|
|
101
|
+
layer,
|
|
102
|
+
preamble: content,
|
|
103
|
+
entries: [],
|
|
104
|
+
legacySections: [],
|
|
105
|
+
parseIssues: [],
|
|
106
|
+
isKnowledgeAdjacent: false,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
preambleEnd = firstEntryLine;
|
|
111
|
+
const preamble = lines.slice(0, preambleEnd).join("\n");
|
|
112
|
+
|
|
113
|
+
// Parse entries and legacy sections
|
|
114
|
+
i = preambleEnd;
|
|
115
|
+
while (i < lines.length) {
|
|
116
|
+
const headingMatch = lines[i].match(ENTRY_HEADING_PATTERN);
|
|
117
|
+
|
|
118
|
+
if (headingMatch) {
|
|
119
|
+
const entryId = headingMatch[1];
|
|
120
|
+
const title = headingMatch[2].trim();
|
|
121
|
+
const entryLineStart = i + 1; // 1-based
|
|
122
|
+
i++;
|
|
123
|
+
|
|
124
|
+
// Look for fenced knowledge-entry block immediately after heading
|
|
125
|
+
let meta: KnowledgeEntryMeta | null = null;
|
|
126
|
+
let metaBlockStart = -1;
|
|
127
|
+
let _metaBlockEnd = -1;
|
|
128
|
+
|
|
129
|
+
// Skip blank lines between heading and fence
|
|
130
|
+
while (i < lines.length && lines[i].trim() === "") {
|
|
131
|
+
i++;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (i < lines.length && lines[i].trim().startsWith("```")) {
|
|
135
|
+
const fenceTag = lines[i].trim().replace(/^```/, "").trim();
|
|
136
|
+
if (fenceTag === KNOWLEDGE_ENTRY_FENCE) {
|
|
137
|
+
metaBlockStart = i + 1; // 1-based line after fence start
|
|
138
|
+
i++;
|
|
139
|
+
const yamlLines: string[] = [];
|
|
140
|
+
while (i < lines.length && !lines[i].trim().startsWith("```")) {
|
|
141
|
+
yamlLines.push(lines[i]);
|
|
142
|
+
i++;
|
|
143
|
+
}
|
|
144
|
+
if (i < lines.length && lines[i].trim().startsWith("```")) {
|
|
145
|
+
_metaBlockEnd = i + 1; // 1-based fence closing line
|
|
146
|
+
i++;
|
|
147
|
+
|
|
148
|
+
const yamlText = yamlLines.join("\n");
|
|
149
|
+
try {
|
|
150
|
+
const raw = parseYaml(yamlText) as Record<string, unknown>;
|
|
151
|
+
const result = knowledgeEntryMetaSchema.safeParse(raw);
|
|
152
|
+
if (result.success) {
|
|
153
|
+
meta = result.data as KnowledgeEntryMeta;
|
|
154
|
+
} else {
|
|
155
|
+
for (const issue of result.error.issues) {
|
|
156
|
+
parseIssues.push({
|
|
157
|
+
line: metaBlockStart,
|
|
158
|
+
message: `Entry ${entryId}: ${issue.message}`,
|
|
159
|
+
entryId,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch (err) {
|
|
164
|
+
parseIssues.push({
|
|
165
|
+
line: metaBlockStart,
|
|
166
|
+
message: `Entry ${entryId}: YAML parse error: ${(err as Error).message}`,
|
|
167
|
+
entryId,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
parseIssues.push({
|
|
172
|
+
line: metaBlockStart,
|
|
173
|
+
message: `Entry ${entryId}: unterminated knowledge-entry fence`,
|
|
174
|
+
entryId,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Collect body until next entry heading or EOF
|
|
181
|
+
const bodyLines: string[] = [];
|
|
182
|
+
while (i < lines.length && !ENTRY_HEADING_PATTERN.test(lines[i])) {
|
|
183
|
+
bodyLines.push(lines[i]);
|
|
184
|
+
i++;
|
|
185
|
+
}
|
|
186
|
+
const body = bodyLines.join("\n").replace(/\n+$/, "");
|
|
187
|
+
|
|
188
|
+
if (meta) {
|
|
189
|
+
entries.push({
|
|
190
|
+
meta,
|
|
191
|
+
title,
|
|
192
|
+
body,
|
|
193
|
+
lineStart: entryLineStart,
|
|
194
|
+
});
|
|
195
|
+
} else if (metaBlockStart === -1) {
|
|
196
|
+
// Heading exists but no metadata block followed — it's a legacy section
|
|
197
|
+
legacySections.push({
|
|
198
|
+
text: lines.slice(entryLineStart - 1, i).join("\n"),
|
|
199
|
+
lineStart: entryLineStart,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
// If meta parsing failed, the parse issue is already recorded
|
|
203
|
+
} else {
|
|
204
|
+
// Non-entry content — collect as legacy section
|
|
205
|
+
const legacyStart = i + 1; // 1-based
|
|
206
|
+
const legacyLines: string[] = [];
|
|
207
|
+
while (i < lines.length && !ENTRY_HEADING_PATTERN.test(lines[i])) {
|
|
208
|
+
legacyLines.push(lines[i]);
|
|
209
|
+
i++;
|
|
210
|
+
}
|
|
211
|
+
legacySections.push({
|
|
212
|
+
text: legacyLines.join("\n"),
|
|
213
|
+
lineStart: legacyStart,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
path: filePath,
|
|
220
|
+
layer,
|
|
221
|
+
preamble,
|
|
222
|
+
entries,
|
|
223
|
+
legacySections,
|
|
224
|
+
parseIssues,
|
|
225
|
+
isKnowledgeAdjacent: false,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -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,51 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { discoverIgnoredFiles, formatSize } from "../ignored-files.ts";
|
|
3
|
+
import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
let tempDir: string;
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
tempDir = await mkdtemp(join(tmpdir(), "forge-ignored-"));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("formatSize", () => {
|
|
18
|
+
it("formats bytes", () => {
|
|
19
|
+
expect(formatSize(0)).toBe("0 B");
|
|
20
|
+
expect(formatSize(512)).toBe("512 B");
|
|
21
|
+
expect(formatSize(1023)).toBe("1023 B");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("formats kilobytes", () => {
|
|
25
|
+
expect(formatSize(1024)).toBe("1.0 KB");
|
|
26
|
+
expect(formatSize(1536)).toBe("1.5 KB");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("formats megabytes", () => {
|
|
30
|
+
expect(formatSize(1024 * 1024)).toBe("1.0 MB");
|
|
31
|
+
expect(formatSize(1024 * 1024 * 5)).toBe("5.0 MB");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("formats gigabytes", () => {
|
|
35
|
+
expect(formatSize(1024 * 1024 * 1024)).toBe("1.0 GB");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("discoverIgnoredFiles", () => {
|
|
40
|
+
it("returns empty array when no .git directory exists", async () => {
|
|
41
|
+
const result = discoverIgnoredFiles(tempDir);
|
|
42
|
+
expect(result).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns empty array when .git exists but no ignored files", async () => {
|
|
46
|
+
await mkdir(join(tempDir, ".git"), { recursive: true });
|
|
47
|
+
await writeFile(join(tempDir, "file.txt"), "content", "utf8");
|
|
48
|
+
const result = discoverIgnoredFiles(tempDir);
|
|
49
|
+
expect(result).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
});
|