@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,448 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Generic invariant enforcement engine — reads check declarations from profile invariants and verifies files against them. Supports filename-pattern, file-contains, file-not-contains, and attribute-pattern check kinds.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not import from @warpgogol/* — this module is portable.</item>
|
|
6
|
+
<item>Do not implement domain-specific validation logic — all rules are declared in profile YAML.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>RFC-0675: initial invariant enforcement engine with three check kinds.</item>
|
|
11
|
+
<item>RFC-0691: add html-attribute-pattern check kind for HTML attribute value validation.</item>
|
|
12
|
+
<item>RFC-0694: replace html-attribute-pattern with attribute-pattern (elements array) for HTML+JSX support.</item>
|
|
13
|
+
<item>RFC-0808: add link-resolution, frontmatter-required, path-exclusion check kinds for note vault validation.</item>
|
|
14
|
+
<item>Add gitignore-entry and secret-scan check kinds for godot-csharp profile.</item>
|
|
15
|
+
</CHANGE_SUMMARY>
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { parse as parseYaml } from "yaml";
|
|
21
|
+
import type { StackProfile } from "../profiles/stack-profile.ts";
|
|
22
|
+
import type { ProfileInvariant } from "../profiles/profile-schema.ts";
|
|
23
|
+
|
|
24
|
+
export interface InvariantViolation {
|
|
25
|
+
invariantId: string;
|
|
26
|
+
severity: "error" | "warning";
|
|
27
|
+
rule: string;
|
|
28
|
+
file: string;
|
|
29
|
+
message: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface InvariantCheckResult {
|
|
33
|
+
invariantId: string;
|
|
34
|
+
severity: "error" | "warning";
|
|
35
|
+
rule: string;
|
|
36
|
+
checked: boolean;
|
|
37
|
+
violations: InvariantViolation[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function matchGlob(filePath: string, glob: string): boolean {
|
|
41
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
42
|
+
const globRegex = globToRegex(glob);
|
|
43
|
+
return globRegex.test(normalizedPath);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function globToRegex(glob: string): RegExp {
|
|
47
|
+
let pattern = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
48
|
+
// Handle **/ : matches zero or more directory segments
|
|
49
|
+
pattern = pattern.replace(/\*\*\//g, "@@GLOBSTAR@@");
|
|
50
|
+
// Handle remaining ** (not followed by /)
|
|
51
|
+
pattern = pattern.replace(/\*\*/g, "@@GLOBSTAR2@@");
|
|
52
|
+
// Handle single * (matches within a path segment, no /)
|
|
53
|
+
pattern = pattern.replace(/\*/g, "[^/]*");
|
|
54
|
+
pattern = pattern.replace(/\?/g, "[^/]");
|
|
55
|
+
// Restore globstars
|
|
56
|
+
pattern = pattern.replace(/@@GLOBSTAR@@/g, "(?:[^/]+/)*");
|
|
57
|
+
pattern = pattern.replace(/@@GLOBSTAR2@@/g, ".*");
|
|
58
|
+
// Handle brace expansion {a,b}
|
|
59
|
+
pattern = pattern.replace(/\\{([^}]+)\\}/g, (_match, content: string) => {
|
|
60
|
+
const options = content.split(",").map((s: string) => s.trim());
|
|
61
|
+
return `(${options.join("|")})`;
|
|
62
|
+
});
|
|
63
|
+
pattern = `^${pattern}$`;
|
|
64
|
+
return new RegExp(pattern);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", ".turbo", "dist", ".cache"]);
|
|
68
|
+
|
|
69
|
+
function collectFiles(dir: string, baseDir: string, results: string[]): void {
|
|
70
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
if (entry.isDirectory() && SKIP_DIRS.has(entry.name)) continue;
|
|
73
|
+
const fullPath = path.join(dir, entry.name);
|
|
74
|
+
const relPath = path.relative(baseDir, fullPath).replace(/\\/g, "/");
|
|
75
|
+
if (entry.isDirectory()) {
|
|
76
|
+
collectFiles(fullPath, baseDir, results);
|
|
77
|
+
} else {
|
|
78
|
+
results.push(relPath);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function compilePattern(
|
|
84
|
+
invariant: ProfileInvariant,
|
|
85
|
+
pattern: string,
|
|
86
|
+
): RegExp | { error: InvariantViolation } {
|
|
87
|
+
try {
|
|
88
|
+
return new RegExp(pattern);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
const e = err as Error;
|
|
91
|
+
return {
|
|
92
|
+
error: {
|
|
93
|
+
invariantId: invariant.id,
|
|
94
|
+
severity: "warning",
|
|
95
|
+
rule: invariant.rule,
|
|
96
|
+
file: "",
|
|
97
|
+
message: `Invariant ${invariant.id} has invalid check pattern: ${e.message}`,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function filterByGlob(files: string[], glob: string): string[] {
|
|
104
|
+
return files.filter((f) => matchGlob(f, glob));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function escapeRegex(str: string): string {
|
|
108
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function runCheck(
|
|
112
|
+
invariant: ProfileInvariant,
|
|
113
|
+
allFiles: string[],
|
|
114
|
+
workspaceRoot: string,
|
|
115
|
+
check: NonNullable<ProfileInvariant["check"]>,
|
|
116
|
+
): InvariantViolation[] {
|
|
117
|
+
const glob = check.glob;
|
|
118
|
+
const pattern = check.pattern ?? check.negatedPattern;
|
|
119
|
+
|
|
120
|
+
if (check.kind === "gitignore-entry") {
|
|
121
|
+
const gitignorePath = path.join(workspaceRoot, ".gitignore");
|
|
122
|
+
try {
|
|
123
|
+
const content = fs.readFileSync(gitignorePath, "utf8");
|
|
124
|
+
const lines = content.split(/\r?\n/).map((l) => l.trim());
|
|
125
|
+
if (!lines.includes(pattern ?? "")) {
|
|
126
|
+
return [{
|
|
127
|
+
invariantId: invariant.id,
|
|
128
|
+
severity: invariant.severity,
|
|
129
|
+
rule: invariant.rule,
|
|
130
|
+
file: ".gitignore",
|
|
131
|
+
message: `.gitignore does not contain entry '${pattern}'`,
|
|
132
|
+
}];
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
return [{
|
|
136
|
+
invariantId: invariant.id,
|
|
137
|
+
severity: invariant.severity,
|
|
138
|
+
rule: invariant.rule,
|
|
139
|
+
file: ".gitignore",
|
|
140
|
+
message: `.gitignore not found`,
|
|
141
|
+
}];
|
|
142
|
+
}
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!glob) return [];
|
|
147
|
+
|
|
148
|
+
const matchedFiles = filterByGlob(allFiles, glob);
|
|
149
|
+
|
|
150
|
+
const needsPattern = check.kind === "filename-pattern" ||
|
|
151
|
+
check.kind === "file-contains" ||
|
|
152
|
+
check.kind === "file-not-contains" ||
|
|
153
|
+
check.kind === "attribute-pattern";
|
|
154
|
+
if (needsPattern && !pattern) return [];
|
|
155
|
+
|
|
156
|
+
const compiled = pattern ? compilePattern(invariant, pattern) : null;
|
|
157
|
+
if (compiled && "error" in compiled) return [compiled.error];
|
|
158
|
+
const regex = compiled && !("error" in compiled) ? compiled : null;
|
|
159
|
+
|
|
160
|
+
const violations: InvariantViolation[] = [];
|
|
161
|
+
|
|
162
|
+
switch (check.kind) {
|
|
163
|
+
case "filename-pattern":
|
|
164
|
+
for (const file of matchedFiles) {
|
|
165
|
+
const filename = path.basename(file);
|
|
166
|
+
if (!regex!.test(filename)) {
|
|
167
|
+
violations.push({
|
|
168
|
+
invariantId: invariant.id,
|
|
169
|
+
severity: invariant.severity,
|
|
170
|
+
rule: invariant.rule,
|
|
171
|
+
file,
|
|
172
|
+
message: `Filename '${filename}' does not match pattern '${pattern}'`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
break;
|
|
177
|
+
case "file-contains":
|
|
178
|
+
for (const file of matchedFiles) {
|
|
179
|
+
const fullPath = path.join(workspaceRoot, file);
|
|
180
|
+
try {
|
|
181
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
182
|
+
if (!regex!.test(content)) {
|
|
183
|
+
violations.push({
|
|
184
|
+
invariantId: invariant.id,
|
|
185
|
+
severity: invariant.severity,
|
|
186
|
+
rule: invariant.rule,
|
|
187
|
+
file,
|
|
188
|
+
message: `File '${file}' does not contain pattern '${pattern}'`,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
// skip unreadable files
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
196
|
+
case "file-not-contains":
|
|
197
|
+
for (const file of matchedFiles) {
|
|
198
|
+
const fullPath = path.join(workspaceRoot, file);
|
|
199
|
+
try {
|
|
200
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
201
|
+
if (regex!.test(content)) {
|
|
202
|
+
violations.push({
|
|
203
|
+
invariantId: invariant.id,
|
|
204
|
+
severity: invariant.severity,
|
|
205
|
+
rule: invariant.rule,
|
|
206
|
+
file,
|
|
207
|
+
message: `File '${file}' contains forbidden pattern '${pattern}'`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
// skip unreadable files
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
case "attribute-pattern": {
|
|
216
|
+
const elements = check.elements;
|
|
217
|
+
const attribute = check.attribute;
|
|
218
|
+
if (!elements || elements.length === 0 || !attribute) {
|
|
219
|
+
violations.push({
|
|
220
|
+
invariantId: invariant.id,
|
|
221
|
+
severity: "warning",
|
|
222
|
+
rule: invariant.rule,
|
|
223
|
+
file: "",
|
|
224
|
+
message: `Invariant ${invariant.id} has attribute-pattern check without elements or attribute`,
|
|
225
|
+
});
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
const elementAlternation = elements.map(escapeRegex).join("|");
|
|
229
|
+
const elementRegex = new RegExp(`<(${elementAlternation})[^>]*>`, "gi");
|
|
230
|
+
const attrRegex = new RegExp(
|
|
231
|
+
`${attribute}="([^"]*)"|${attribute}='([^']*)'`,
|
|
232
|
+
"i",
|
|
233
|
+
);
|
|
234
|
+
for (const file of matchedFiles) {
|
|
235
|
+
const fullPath = path.join(workspaceRoot, file);
|
|
236
|
+
try {
|
|
237
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
238
|
+
elementRegex.lastIndex = 0;
|
|
239
|
+
let match: RegExpExecArray | null;
|
|
240
|
+
while ((match = elementRegex.exec(content)) !== null) {
|
|
241
|
+
const elementSnippet = match[0];
|
|
242
|
+
const matchedElement = match[1];
|
|
243
|
+
const attrMatch = attrRegex.exec(elementSnippet);
|
|
244
|
+
if (!attrMatch) continue;
|
|
245
|
+
const attrValue = attrMatch[1] ?? attrMatch[2] ?? "";
|
|
246
|
+
if (!regex!.test(attrValue)) {
|
|
247
|
+
violations.push({
|
|
248
|
+
invariantId: invariant.id,
|
|
249
|
+
severity: invariant.severity,
|
|
250
|
+
rule: invariant.rule,
|
|
251
|
+
file,
|
|
252
|
+
message: `Element <${matchedElement}> attribute '${attribute}' value '${attrValue}' does not match pattern '${pattern}'`,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} catch {
|
|
257
|
+
// skip unreadable files
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
case "link-resolution": {
|
|
263
|
+
const allMdFiles = allFiles.filter((f) => f.endsWith(".md"));
|
|
264
|
+
const noteMap = new Map<string, string>();
|
|
265
|
+
for (const mdFile of allMdFiles) {
|
|
266
|
+
const basename = path.basename(mdFile, ".md");
|
|
267
|
+
noteMap.set(basename, mdFile);
|
|
268
|
+
const fullPath = path.join(workspaceRoot, mdFile);
|
|
269
|
+
try {
|
|
270
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
271
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
272
|
+
if (fmMatch) {
|
|
273
|
+
try {
|
|
274
|
+
const fm = parseYaml(fmMatch[1]) as Record<string, unknown>;
|
|
275
|
+
if (fm.aliases && Array.isArray(fm.aliases)) {
|
|
276
|
+
for (const alias of fm.aliases) {
|
|
277
|
+
if (typeof alias === "string") noteMap.set(alias, mdFile);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} catch {
|
|
281
|
+
// skip malformed frontmatter
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
// skip unreadable files
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const linkRegex = /\[\[([^\]]+?)\]\]/g;
|
|
289
|
+
for (const file of matchedFiles) {
|
|
290
|
+
const fullPath = path.join(workspaceRoot, file);
|
|
291
|
+
try {
|
|
292
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
293
|
+
const lines = content.split(/\r?\n/);
|
|
294
|
+
for (let i = 0; i < lines.length; i++) {
|
|
295
|
+
let match: RegExpExecArray | null;
|
|
296
|
+
linkRegex.lastIndex = 0;
|
|
297
|
+
while ((match = linkRegex.exec(lines[i])) !== null) {
|
|
298
|
+
const linkTarget = match[1].split("|")[0].split("#")[0].trim();
|
|
299
|
+
if (!linkTarget) continue;
|
|
300
|
+
if (!noteMap.has(linkTarget)) {
|
|
301
|
+
violations.push({
|
|
302
|
+
invariantId: invariant.id,
|
|
303
|
+
severity: invariant.severity,
|
|
304
|
+
rule: invariant.rule,
|
|
305
|
+
file,
|
|
306
|
+
message: `Wikilink target '${linkTarget}' not found in vault`,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} catch {
|
|
312
|
+
// skip unreadable files
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
case "frontmatter-required": {
|
|
318
|
+
const fields = check.fields;
|
|
319
|
+
if (!fields || fields.length === 0) break;
|
|
320
|
+
for (const file of matchedFiles) {
|
|
321
|
+
const fullPath = path.join(workspaceRoot, file);
|
|
322
|
+
try {
|
|
323
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
324
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
325
|
+
if (!fmMatch) {
|
|
326
|
+
violations.push({
|
|
327
|
+
invariantId: invariant.id,
|
|
328
|
+
severity: invariant.severity,
|
|
329
|
+
rule: invariant.rule,
|
|
330
|
+
file,
|
|
331
|
+
message: `File '${file}' has no frontmatter`,
|
|
332
|
+
});
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
let fm: Record<string, unknown> = {};
|
|
336
|
+
try {
|
|
337
|
+
fm = parseYaml(fmMatch[1]) as Record<string, unknown>;
|
|
338
|
+
} catch {
|
|
339
|
+
violations.push({
|
|
340
|
+
invariantId: invariant.id,
|
|
341
|
+
severity: invariant.severity,
|
|
342
|
+
rule: invariant.rule,
|
|
343
|
+
file,
|
|
344
|
+
message: `File '${file}' has malformed frontmatter`,
|
|
345
|
+
});
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
for (const field of fields) {
|
|
349
|
+
if (!(field in fm) || fm[field] == null || fm[field] === "") {
|
|
350
|
+
const hasH1 = /^\s*#\s+\S/.test(content.slice(fmMatch[0].length));
|
|
351
|
+
if (field === "title" && hasH1) continue;
|
|
352
|
+
violations.push({
|
|
353
|
+
invariantId: invariant.id,
|
|
354
|
+
severity: invariant.severity,
|
|
355
|
+
rule: invariant.rule,
|
|
356
|
+
file,
|
|
357
|
+
message: `File '${file}' missing required field '${field}'`,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
// skip unreadable files
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
case "path-exclusion": {
|
|
368
|
+
for (const file of matchedFiles) {
|
|
369
|
+
violations.push({
|
|
370
|
+
invariantId: invariant.id,
|
|
371
|
+
severity: invariant.severity,
|
|
372
|
+
rule: invariant.rule,
|
|
373
|
+
file,
|
|
374
|
+
message: `File '${file}' matches excluded pattern`,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
case "secret-scan": {
|
|
380
|
+
const secretPatterns = [
|
|
381
|
+
/(?:api[_-]?key|apikey)["'\s]*[:=]["'\s]*["'`]([A-Za-z0-9_\-]{20,})["'`]/gi,
|
|
382
|
+
/(?:secret|token|password|passwd)["'\s]*[:=]["'\s]*["'`]([^"'`\s]{8,})["'`]/gi,
|
|
383
|
+
/-----BEGIN [A-Z]+ PRIVATE KEY-----/g,
|
|
384
|
+
/sk_[a-zA-Z0-9]{20,}/g,
|
|
385
|
+
/gh[pousr]_[A-Za-z0-9]{36,}/g,
|
|
386
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
387
|
+
];
|
|
388
|
+
for (const file of matchedFiles) {
|
|
389
|
+
const fullPath = path.join(workspaceRoot, file);
|
|
390
|
+
try {
|
|
391
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
392
|
+
for (const secretPattern of secretPatterns) {
|
|
393
|
+
secretPattern.lastIndex = 0;
|
|
394
|
+
let match: RegExpExecArray | null;
|
|
395
|
+
while ((match = secretPattern.exec(content)) !== null) {
|
|
396
|
+
violations.push({
|
|
397
|
+
invariantId: invariant.id,
|
|
398
|
+
severity: invariant.severity,
|
|
399
|
+
rule: invariant.rule,
|
|
400
|
+
file,
|
|
401
|
+
message: `Potential secret detected: ${match[0].slice(0, 30)}...`,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
} catch {
|
|
406
|
+
// skip unreadable files
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return violations;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function checkInvariants(
|
|
417
|
+
profile: StackProfile,
|
|
418
|
+
workspaceRoot: string,
|
|
419
|
+
): InvariantCheckResult[] {
|
|
420
|
+
if (!profile.invariants || profile.invariants.length === 0) {
|
|
421
|
+
return [];
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const allFiles: string[] = [];
|
|
425
|
+
collectFiles(workspaceRoot, workspaceRoot, allFiles);
|
|
426
|
+
|
|
427
|
+
return profile.invariants.map((invariant) => {
|
|
428
|
+
if (!invariant.check) {
|
|
429
|
+
return {
|
|
430
|
+
invariantId: invariant.id,
|
|
431
|
+
severity: invariant.severity,
|
|
432
|
+
rule: invariant.rule,
|
|
433
|
+
checked: false,
|
|
434
|
+
violations: [],
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const violations = runCheck(invariant, allFiles, workspaceRoot, invariant.check);
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
invariantId: invariant.id,
|
|
442
|
+
severity: invariant.severity,
|
|
443
|
+
rule: invariant.rule,
|
|
444
|
+
checked: true,
|
|
445
|
+
violations,
|
|
446
|
+
};
|
|
447
|
+
});
|
|
448
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Memory layer scaffold and health check (RFC-0664). Creates .agents/memory/MEMORY.md, .agents/memory/daily/.gitkeep, and a marker-delimited .gitignore block. Reports health for forge.doctor.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not overwrite existing MEMORY.md or daily/ files — idempotent only.</item>
|
|
6
|
+
<item>Do not enforce the read discipline — that is advisory, via AGENTS.md rules.</item>
|
|
7
|
+
<item>Do not change doctor's exit code — memory-layer warnings are advisory only.</item>
|
|
8
|
+
</non-goals>
|
|
9
|
+
</MODULE_CONTRACT>
|
|
10
|
+
<CHANGE_SUMMARY>
|
|
11
|
+
<item>RFC-0664: initial memory layer scaffold and health check.</item>
|
|
12
|
+
</CHANGE_SUMMARY>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { loadForgeConfig } from "../config/forge-config.ts";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Constants
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export const MEMORY_GITIGNORE_START = "# forge-agent-memory";
|
|
24
|
+
export const MEMORY_GITIGNORE_END = "# /forge-agent-memory";
|
|
25
|
+
|
|
26
|
+
export const DEFAULT_MEMORY_BUDGET = 4096;
|
|
27
|
+
|
|
28
|
+
const MEMORY_DIR = ".agents/memory";
|
|
29
|
+
const DAILY_DIR = "daily";
|
|
30
|
+
const MEMORY_MD = "MEMORY.md";
|
|
31
|
+
const GITKEEP = ".gitkeep";
|
|
32
|
+
|
|
33
|
+
const MEMORY_MD_TEMPLATE = `# Project Memory
|
|
34
|
+
|
|
35
|
+
Curated project context (RFC-0664). This file is versioned — daily logs in \`daily/\` are git-ignored.
|
|
36
|
+
|
|
37
|
+
## Current focus
|
|
38
|
+
|
|
39
|
+
<!-- What is being worked on right now. One to three bullets max. -->
|
|
40
|
+
|
|
41
|
+
## Decisions in flight
|
|
42
|
+
|
|
43
|
+
<!-- Decisions under discussion but not yet final. -->
|
|
44
|
+
|
|
45
|
+
## Environment notes
|
|
46
|
+
|
|
47
|
+
<!-- Tool versions, environment quirks, known issues. -->
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Types
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
export interface MemoryScaffoldResult {
|
|
55
|
+
created: string[];
|
|
56
|
+
gitignoreUpdated: boolean;
|
|
57
|
+
skipped: string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface MemoryLayerHealth {
|
|
61
|
+
memoryMdChars: number;
|
|
62
|
+
budget: number;
|
|
63
|
+
gitignoreCoversDaily: boolean;
|
|
64
|
+
dailyFileCount: number;
|
|
65
|
+
memoryMdExists: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Scaffold
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
function hasGitignoreBlock(content: string): boolean {
|
|
73
|
+
return content.includes(MEMORY_GITIGNORE_START) && content.includes(MEMORY_GITIGNORE_END);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function insertGitignoreBlock(existingContent: string): string {
|
|
77
|
+
const block = `${MEMORY_GITIGNORE_START}\n${MEMORY_DIR}/${DAILY_DIR}/\n${MEMORY_GITIGNORE_END}`;
|
|
78
|
+
|
|
79
|
+
if (existingContent.length === 0) {
|
|
80
|
+
return block + "\n";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Ensure existing content ends with a newline before appending
|
|
84
|
+
const trimmed = existingContent.endsWith("\n") ? existingContent : existingContent + "\n";
|
|
85
|
+
return trimmed + "\n" + block + "\n";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function scaffoldMemoryLayer(workspaceRoot: string): MemoryScaffoldResult {
|
|
89
|
+
const created: string[] = [];
|
|
90
|
+
const skipped: string[] = [];
|
|
91
|
+
let gitignoreUpdated = false;
|
|
92
|
+
|
|
93
|
+
const memoryDir = path.join(workspaceRoot, MEMORY_DIR);
|
|
94
|
+
const dailyDirPath = path.join(memoryDir, DAILY_DIR);
|
|
95
|
+
const memoryMdPath = path.join(memoryDir, MEMORY_MD);
|
|
96
|
+
const gitkeepPath = path.join(dailyDirPath, GITKEEP);
|
|
97
|
+
|
|
98
|
+
// Create .agents/memory/ directory
|
|
99
|
+
fs.mkdirSync(memoryDir, { recursive: true });
|
|
100
|
+
fs.mkdirSync(dailyDirPath, { recursive: true });
|
|
101
|
+
|
|
102
|
+
// MEMORY.md — create only if absent
|
|
103
|
+
if (fs.existsSync(memoryMdPath)) {
|
|
104
|
+
skipped.push(MEMORY_MD);
|
|
105
|
+
} else {
|
|
106
|
+
fs.writeFileSync(memoryMdPath, MEMORY_MD_TEMPLATE, "utf8");
|
|
107
|
+
created.push(`${MEMORY_DIR}/${MEMORY_MD}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// daily/.gitkeep — create only if absent
|
|
111
|
+
if (fs.existsSync(gitkeepPath)) {
|
|
112
|
+
skipped.push(`${DAILY_DIR}/${GITKEEP}`);
|
|
113
|
+
} else {
|
|
114
|
+
fs.writeFileSync(gitkeepPath, "", "utf8");
|
|
115
|
+
created.push(`${MEMORY_DIR}/${DAILY_DIR}/${GITKEEP}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// .gitignore — add marker block if absent
|
|
119
|
+
const gitignorePath = path.join(workspaceRoot, ".gitignore");
|
|
120
|
+
if (fs.existsSync(gitignorePath)) {
|
|
121
|
+
const existing = fs.readFileSync(gitignorePath, "utf8");
|
|
122
|
+
if (hasGitignoreBlock(existing)) {
|
|
123
|
+
// Block already present — no update needed
|
|
124
|
+
} else {
|
|
125
|
+
const updated = insertGitignoreBlock(existing);
|
|
126
|
+
fs.writeFileSync(gitignorePath, updated, "utf8");
|
|
127
|
+
gitignoreUpdated = true;
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
fs.writeFileSync(gitignorePath, `${MEMORY_GITIGNORE_START}\n${MEMORY_DIR}/${DAILY_DIR}/\n${MEMORY_GITIGNORE_END}\n`, "utf8");
|
|
131
|
+
gitignoreUpdated = true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { created, gitignoreUpdated, skipped };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Health check
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
function resolveMemoryBudget(workspaceRoot: string): number {
|
|
142
|
+
try {
|
|
143
|
+
const config = loadForgeConfig(workspaceRoot);
|
|
144
|
+
if (config.bindings?.memory?.budget !== undefined && config.bindings?.memory?.budget !== null) {
|
|
145
|
+
return config.bindings.memory.budget;
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
// forge.yaml not loadable — use default
|
|
149
|
+
}
|
|
150
|
+
return DEFAULT_MEMORY_BUDGET;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function checkMemoryLayerHealth(workspaceRoot: string): MemoryLayerHealth {
|
|
154
|
+
const memoryDir = path.join(workspaceRoot, MEMORY_DIR);
|
|
155
|
+
const dailyDirPath = path.join(memoryDir, DAILY_DIR);
|
|
156
|
+
const memoryMdPath = path.join(memoryDir, MEMORY_MD);
|
|
157
|
+
const gitignorePath = path.join(workspaceRoot, ".gitignore");
|
|
158
|
+
|
|
159
|
+
// MEMORY.md char count
|
|
160
|
+
let memoryMdChars = 0;
|
|
161
|
+
const memoryMdExists = fs.existsSync(memoryMdPath);
|
|
162
|
+
if (memoryMdExists) {
|
|
163
|
+
memoryMdChars = fs.readFileSync(memoryMdPath, "utf8").length;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Daily file count (exclude .gitkeep)
|
|
167
|
+
let dailyFileCount = 0;
|
|
168
|
+
if (fs.existsSync(dailyDirPath)) {
|
|
169
|
+
const entries = fs.readdirSync(dailyDirPath);
|
|
170
|
+
dailyFileCount = entries.filter((e) => e !== GITKEEP).length;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Gitignore coverage
|
|
174
|
+
let gitignoreCoversDaily = false;
|
|
175
|
+
if (fs.existsSync(gitignorePath)) {
|
|
176
|
+
const content = fs.readFileSync(gitignorePath, "utf8");
|
|
177
|
+
gitignoreCoversDaily = hasGitignoreBlock(content);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const budget = resolveMemoryBudget(workspaceRoot);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
memoryMdChars,
|
|
184
|
+
budget,
|
|
185
|
+
gitignoreCoversDaily,
|
|
186
|
+
dailyFileCount,
|
|
187
|
+
memoryMdExists,
|
|
188
|
+
};
|
|
189
|
+
}
|