truthmark 2.2.7 → 2.3.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/README.md +119 -166
- package/dist/main.js +2156 -1914
- package/dist/main.js.map +1 -1
- package/docs/README.md +49 -37
- package/docs/assets/truthmark-workflow-mobile.svg +118 -0
- package/docs/assets/truthmark-workflow.svg +182 -0
- package/docs/readmes/README.ar.md +125 -160
- package/docs/readmes/README.de.md +127 -162
- package/docs/readmes/README.el.md +127 -162
- package/docs/readmes/README.es.md +125 -160
- package/docs/readmes/README.fr.md +125 -160
- package/docs/readmes/README.id.md +126 -161
- package/docs/readmes/README.it.md +125 -160
- package/docs/readmes/README.ja.md +126 -161
- package/docs/readmes/README.ko.md +127 -162
- package/docs/readmes/README.pl.md +125 -160
- package/docs/readmes/README.pt.md +126 -161
- package/docs/readmes/README.ru.md +124 -159
- package/docs/readmes/README.tr.md +125 -160
- package/docs/readmes/README.vi.md +126 -161
- package/docs/readmes/README.zh.md +128 -163
- package/package.json +3 -1
package/dist/main.js
CHANGED
|
@@ -42,8 +42,14 @@ var renderJson = (result) => {
|
|
|
42
42
|
return JSON.stringify(toStableValue(result), null, 2);
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
// src/
|
|
46
|
-
import
|
|
45
|
+
// src/init/init.ts
|
|
46
|
+
import fs6 from "fs/promises";
|
|
47
|
+
|
|
48
|
+
// src/config/load.ts
|
|
49
|
+
import fs2 from "fs/promises";
|
|
50
|
+
import path2 from "path";
|
|
51
|
+
import { Ajv } from "ajv";
|
|
52
|
+
import { parse } from "yaml";
|
|
47
53
|
|
|
48
54
|
// src/fs/paths.ts
|
|
49
55
|
import path from "path";
|
|
@@ -54,6 +60,7 @@ var isPathInsideRoot = (rootDir, targetPath) => {
|
|
|
54
60
|
var isNodeErrorWithCode = (error, code) => {
|
|
55
61
|
return error instanceof Error && "code" in error && error.code === code;
|
|
56
62
|
};
|
|
63
|
+
var isExcludedPath = (targetPath, excludedRoots) => excludedRoots.some((root) => isPathInsideRoot(root, targetPath));
|
|
57
64
|
var pathSegments = (absolutePath) => {
|
|
58
65
|
return absolutePath.split(path.sep);
|
|
59
66
|
};
|
|
@@ -101,10 +108,15 @@ var resolveRepoPath = (rootDir, relativePath) => {
|
|
|
101
108
|
}
|
|
102
109
|
return resolvedPath;
|
|
103
110
|
};
|
|
104
|
-
var isSafeExactFile = async (rootDir, relativePath, allowMissing) => {
|
|
111
|
+
var isSafeExactFile = async (rootDir, relativePath, allowMissing, excludedRoots = []) => {
|
|
105
112
|
try {
|
|
106
113
|
const absolutePath = resolveRepoPath(rootDir, relativePath);
|
|
107
114
|
await assertRepoContainment(rootDir, absolutePath);
|
|
115
|
+
if (isExcludedPath(
|
|
116
|
+
await resolveThroughExistingAncestor(absolutePath),
|
|
117
|
+
excludedRoots
|
|
118
|
+
))
|
|
119
|
+
return false;
|
|
108
120
|
const relative = path.relative(rootDir, absolutePath);
|
|
109
121
|
const segments = pathSegments(relative);
|
|
110
122
|
if (segments.length === 0 || segments.every((segment) => segment.length === 0)) {
|
|
@@ -138,6 +150,43 @@ var isSafeExactFile = async (rootDir, relativePath, allowMissing) => {
|
|
|
138
150
|
return false;
|
|
139
151
|
}
|
|
140
152
|
};
|
|
153
|
+
var resolveSafeExactFileTarget = async (rootDir, relativePath, allowMissing, allowFinalSymlink, excludedRoots = []) => {
|
|
154
|
+
if (await isSafeExactFile(rootDir, relativePath, allowMissing, excludedRoots)) {
|
|
155
|
+
const absolutePath = resolveRepoPath(rootDir, relativePath);
|
|
156
|
+
try {
|
|
157
|
+
const [resolvedRootDir, resolvedPath] = await Promise.all([
|
|
158
|
+
fs.realpath(rootDir),
|
|
159
|
+
fs.realpath(absolutePath)
|
|
160
|
+
]);
|
|
161
|
+
return {
|
|
162
|
+
path: toRepoRelativePath(resolvedRootDir, resolvedPath),
|
|
163
|
+
aliased: false
|
|
164
|
+
};
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (!allowMissing || !isNodeErrorWithCode(error, "ENOENT")) return null;
|
|
167
|
+
return {
|
|
168
|
+
path: toRepoRelativePath(rootDir, absolutePath),
|
|
169
|
+
aliased: false
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!allowFinalSymlink) return null;
|
|
174
|
+
try {
|
|
175
|
+
const absolutePath = resolveRepoPath(rootDir, relativePath);
|
|
176
|
+
if (!(await fs.lstat(absolutePath)).isSymbolicLink()) return null;
|
|
177
|
+
const resolvedPath = await fs.realpath(absolutePath);
|
|
178
|
+
await assertRepoContainment(rootDir, resolvedPath);
|
|
179
|
+
if (isExcludedPath(resolvedPath, excludedRoots)) return null;
|
|
180
|
+
const stat = await fs.lstat(resolvedPath);
|
|
181
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) return null;
|
|
182
|
+
return {
|
|
183
|
+
path: toRepoRelativePath(await fs.realpath(rootDir), resolvedPath),
|
|
184
|
+
aliased: true
|
|
185
|
+
};
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
141
190
|
var assertRepoContainment = async (rootDir, targetPath) => {
|
|
142
191
|
const [resolvedRootDir, resolvedTargetPath] = await Promise.all([
|
|
143
192
|
resolveThroughExistingAncestor(rootDir),
|
|
@@ -213,81 +262,6 @@ var ensureRepoFile = async (rootDir, relativePath, content) => {
|
|
|
213
262
|
};
|
|
214
263
|
};
|
|
215
264
|
|
|
216
|
-
// src/git/repository.ts
|
|
217
|
-
import fs2 from "fs/promises";
|
|
218
|
-
import { realpathSync } from "fs";
|
|
219
|
-
import path2 from "path";
|
|
220
|
-
import { execa } from "execa";
|
|
221
|
-
var realpathOrResolved = async (targetPath) => {
|
|
222
|
-
try {
|
|
223
|
-
return await fs2.realpath(targetPath);
|
|
224
|
-
} catch {
|
|
225
|
-
return path2.resolve(targetPath);
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
var runGit = async (cwd, args, reject = true) => {
|
|
229
|
-
const result = await execa("git", args, { cwd, reject });
|
|
230
|
-
return {
|
|
231
|
-
stdout: result.stdout,
|
|
232
|
-
exitCode: result.exitCode ?? 1
|
|
233
|
-
};
|
|
234
|
-
};
|
|
235
|
-
var getGitRepository = async (cwd) => {
|
|
236
|
-
const worktreePath = await realpathOrResolved(
|
|
237
|
-
(await runGit(cwd, ["rev-parse", "--show-toplevel"])).stdout.trim()
|
|
238
|
-
);
|
|
239
|
-
const commonDirOutput = (await runGit(cwd, ["rev-parse", "--git-common-dir"])).stdout.trim();
|
|
240
|
-
const commonDir = await realpathOrResolved(path2.resolve(worktreePath, commonDirOutput));
|
|
241
|
-
const repositoryRoot = path2.basename(commonDir) === ".git" ? path2.dirname(commonDir) : worktreePath;
|
|
242
|
-
const branchResult = await runGit(cwd, ["symbolic-ref", "--quiet", "--short", "HEAD"], false);
|
|
243
|
-
const headResult = await runGit(cwd, ["rev-parse", "--verify", "HEAD"], false);
|
|
244
|
-
const branchName = branchResult.exitCode === 0 ? branchResult.stdout.trim() : null;
|
|
245
|
-
const headSha = headResult.exitCode === 0 ? headResult.stdout.trim() : null;
|
|
246
|
-
const isDetached = branchName === null;
|
|
247
|
-
const isUnborn = !isDetached && headSha === null;
|
|
248
|
-
return {
|
|
249
|
-
repositoryRoot,
|
|
250
|
-
worktreePath,
|
|
251
|
-
branchName,
|
|
252
|
-
headSha,
|
|
253
|
-
isDetached,
|
|
254
|
-
isUnborn
|
|
255
|
-
};
|
|
256
|
-
};
|
|
257
|
-
var resolveWorktreePath = (repository, relativePath) => {
|
|
258
|
-
const resolvedPath = path2.resolve(repository.worktreePath, relativePath);
|
|
259
|
-
let currentPath = resolvedPath;
|
|
260
|
-
const missingSegments = [];
|
|
261
|
-
const resolveContainedPath = () => {
|
|
262
|
-
while (true) {
|
|
263
|
-
try {
|
|
264
|
-
return missingSegments.reduceRight((resolvedExistingPath, segment) => {
|
|
265
|
-
return path2.join(resolvedExistingPath, segment);
|
|
266
|
-
}, realpathSync(currentPath));
|
|
267
|
-
} catch (error) {
|
|
268
|
-
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
269
|
-
throw error;
|
|
270
|
-
}
|
|
271
|
-
const parentPath = path2.dirname(currentPath);
|
|
272
|
-
if (parentPath === currentPath) {
|
|
273
|
-
return resolvedPath;
|
|
274
|
-
}
|
|
275
|
-
missingSegments.unshift(path2.basename(currentPath));
|
|
276
|
-
currentPath = parentPath;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
const containedPath = resolveContainedPath();
|
|
281
|
-
if (containedPath !== repository.worktreePath && !containedPath.startsWith(`${repository.worktreePath}${path2.sep}`)) {
|
|
282
|
-
throw new Error("resolved path must stay inside the active worktree");
|
|
283
|
-
}
|
|
284
|
-
return resolvedPath;
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
// src/templates/init-files.ts
|
|
288
|
-
import path3 from "path";
|
|
289
|
-
import { stringify } from "yaml";
|
|
290
|
-
|
|
291
265
|
// src/config/schema.ts
|
|
292
266
|
var SUPPORTED_PLATFORMS = [
|
|
293
267
|
"codex",
|
|
@@ -457,348 +431,276 @@ var createDefaultConfig = () => ({
|
|
|
457
431
|
ignore: ["node_modules/**", "vendor/**", "dist/**", "build/**"]
|
|
458
432
|
});
|
|
459
433
|
|
|
460
|
-
// src/
|
|
461
|
-
|
|
462
|
-
var
|
|
463
|
-
|
|
464
|
-
"
|
|
465
|
-
"
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
"engineering-operations",
|
|
469
|
-
"engineering-test-behavior"
|
|
470
|
-
];
|
|
471
|
-
var uniqueSorted = (values) => [...new Set(values)].sort();
|
|
472
|
-
var mergeTruthDocumentEntryRelationships = (first, second) => ({
|
|
473
|
-
...first,
|
|
474
|
-
realizedBy: uniqueSorted([...first.realizedBy, ...second.realizedBy]),
|
|
475
|
-
realizes: uniqueSorted([...first.realizes, ...second.realizes]),
|
|
476
|
-
dependsOn: uniqueSorted([...first.dependsOn, ...second.dependsOn])
|
|
434
|
+
// src/config/load.ts
|
|
435
|
+
var ajv = new Ajv({ allErrors: true });
|
|
436
|
+
var validateTruthmarkConfig = ajv.compile(truthmarkConfigSchema);
|
|
437
|
+
var toConfigDiagnostic = (message, file) => ({
|
|
438
|
+
category: "config",
|
|
439
|
+
severity: "error",
|
|
440
|
+
message,
|
|
441
|
+
file
|
|
477
442
|
});
|
|
478
|
-
var
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
482
|
-
};
|
|
483
|
-
var createAreaDiagnostic = (message, area, severity = "error") => {
|
|
484
|
-
return {
|
|
485
|
-
category: "area-index",
|
|
486
|
-
severity,
|
|
487
|
-
message,
|
|
488
|
-
area
|
|
489
|
-
};
|
|
443
|
+
var normalizeRepoRelativePath = (value) => {
|
|
444
|
+
const slashNormalized = value.replace(/\\/gu, "/");
|
|
445
|
+
return path2.posix.normalize(slashNormalized).replace(/\/+$/u, "");
|
|
490
446
|
};
|
|
491
|
-
var
|
|
492
|
-
|
|
447
|
+
var isUnsafeRepoRelativePath = (value) => {
|
|
448
|
+
const slashNormalized = value.replace(/\\/gu, "/");
|
|
449
|
+
const normalized = normalizeRepoRelativePath(value);
|
|
450
|
+
const parts = slashNormalized.split("/");
|
|
451
|
+
return normalized.length === 0 || normalized === "." || normalized === ".." || path2.isAbsolute(value) || path2.posix.isAbsolute(slashNormalized) || path2.win32.isAbsolute(value) || /^[A-Za-z]:/u.test(value) || normalized.startsWith("../") || parts.includes("..");
|
|
493
452
|
};
|
|
494
|
-
var
|
|
495
|
-
return
|
|
453
|
+
var joinWorkspacePath = (workspace, childPath) => {
|
|
454
|
+
return normalizeRepoRelativePath(`${workspace}/${childPath}`);
|
|
496
455
|
};
|
|
497
|
-
var
|
|
498
|
-
|
|
499
|
-
const
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
return "product-capability";
|
|
503
|
-
}
|
|
504
|
-
if (engineeringTruthRoot && normalizedPath.startsWith(`${engineeringTruthRoot}/`)) {
|
|
505
|
-
if (normalizedPath.includes("/contracts/")) return "engineering-contract";
|
|
506
|
-
if (normalizedPath.includes("/workflows/")) return "engineering-workflow";
|
|
507
|
-
if (normalizedPath.includes("/architecture/"))
|
|
508
|
-
return "engineering-architecture";
|
|
509
|
-
if (normalizedPath.includes("/operations/"))
|
|
510
|
-
return "engineering-operations";
|
|
511
|
-
if (normalizedPath.includes("/tests/")) return "engineering-test-behavior";
|
|
512
|
-
return "engineering-behavior";
|
|
513
|
-
}
|
|
514
|
-
return null;
|
|
456
|
+
var portalOutputFor = (workspace) => joinWorkspacePath(workspace, DERIVED_TRUTHMARK_PATHS.portalOutput);
|
|
457
|
+
var pathsOverlap = (left, right) => {
|
|
458
|
+
const normalizedLeft = normalizeRepoRelativePath(left);
|
|
459
|
+
const normalizedRight = normalizeRepoRelativePath(right);
|
|
460
|
+
return normalizedLeft === normalizedRight || normalizedLeft.startsWith(`${normalizedRight}/`) || normalizedRight.startsWith(`${normalizedLeft}/`);
|
|
515
461
|
};
|
|
516
|
-
var
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
462
|
+
var CONFIG_PATH = ".truthmark/config.yml";
|
|
463
|
+
var FORBIDDEN_WORKSPACE_OVERLAPS = [
|
|
464
|
+
".git",
|
|
465
|
+
".truthmark",
|
|
466
|
+
"package.json",
|
|
467
|
+
"package-lock.json",
|
|
468
|
+
"pnpm-lock.yaml",
|
|
469
|
+
"yarn.lock",
|
|
470
|
+
"src",
|
|
471
|
+
"tests"
|
|
472
|
+
];
|
|
473
|
+
var unsupportedShapeDiagnostics = (parsedConfig, configPath) => {
|
|
474
|
+
if (!parsedConfig || typeof parsedConfig !== "object" || Array.isArray(parsedConfig)) {
|
|
475
|
+
return [];
|
|
522
476
|
}
|
|
523
|
-
|
|
524
|
-
|
|
477
|
+
const record = parsedConfig;
|
|
478
|
+
const diagnostics = [];
|
|
479
|
+
if (record.version !== 2) {
|
|
480
|
+
diagnostics.push(
|
|
481
|
+
toConfigDiagnostic(
|
|
482
|
+
"Unsupported Truthmark config shape. This release requires version: 2 with a truthmark workspace block.",
|
|
483
|
+
configPath
|
|
484
|
+
)
|
|
485
|
+
);
|
|
525
486
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
return "product";
|
|
487
|
+
if ("docs" in record || "authority" in record) {
|
|
488
|
+
diagnostics.push(
|
|
489
|
+
toConfigDiagnostic(
|
|
490
|
+
"Unsupported Truthmark config shape. Remove old docs.roots and legacy authority settings; use version: 2 truthmark.workspace paths.",
|
|
491
|
+
configPath
|
|
492
|
+
)
|
|
493
|
+
);
|
|
534
494
|
}
|
|
535
|
-
return
|
|
495
|
+
return diagnostics;
|
|
536
496
|
};
|
|
537
|
-
var
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
497
|
+
var validateWorkspacePaths = (rawConfig, configPath) => {
|
|
498
|
+
const diagnostics = [];
|
|
499
|
+
const workspace = normalizeRepoRelativePath(rawConfig.truthmark.workspace);
|
|
500
|
+
if (isUnsafeRepoRelativePath(rawConfig.truthmark.workspace) || FORBIDDEN_WORKSPACE_OVERLAPS.some(
|
|
501
|
+
(forbidden) => pathsOverlap(workspace, forbidden)
|
|
502
|
+
)) {
|
|
503
|
+
diagnostics.push(
|
|
504
|
+
toConfigDiagnostic(
|
|
505
|
+
"truthmark.workspace must be a non-empty repo-relative directory that does not overlap repository control, package, source, test, or instruction paths.",
|
|
506
|
+
configPath
|
|
507
|
+
)
|
|
508
|
+
);
|
|
544
509
|
}
|
|
545
|
-
return
|
|
510
|
+
return diagnostics;
|
|
546
511
|
};
|
|
547
|
-
var
|
|
548
|
-
const
|
|
549
|
-
const
|
|
550
|
-
|
|
512
|
+
var normalizeConfig = (rawConfig) => {
|
|
513
|
+
const workspace = normalizeRepoRelativePath(rawConfig.truthmark.workspace);
|
|
514
|
+
const routesIndex = joinWorkspacePath(
|
|
515
|
+
workspace,
|
|
516
|
+
DERIVED_TRUTHMARK_PATHS.routesIndex
|
|
551
517
|
);
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
518
|
+
const routeAreasRoot = joinWorkspacePath(
|
|
519
|
+
workspace,
|
|
520
|
+
DERIVED_TRUTHMARK_PATHS.routeAreasRoot
|
|
521
|
+
);
|
|
522
|
+
const productTruthRoot = joinWorkspacePath(
|
|
523
|
+
workspace,
|
|
524
|
+
DERIVED_TRUTHMARK_PATHS.productTruthRoot
|
|
525
|
+
);
|
|
526
|
+
const engineeringTruthRoot = joinWorkspacePath(
|
|
527
|
+
workspace,
|
|
528
|
+
DERIVED_TRUTHMARK_PATHS.engineeringTruthRoot
|
|
529
|
+
);
|
|
530
|
+
const templatesRoot = joinWorkspacePath(
|
|
531
|
+
workspace,
|
|
532
|
+
DERIVED_TRUTHMARK_PATHS.templatesRoot
|
|
533
|
+
);
|
|
534
|
+
const portalOutput = portalOutputFor(workspace);
|
|
535
|
+
const portalTemplate = joinWorkspacePath(
|
|
536
|
+
workspace,
|
|
537
|
+
DERIVED_TRUTHMARK_PATHS.portalTemplate
|
|
557
538
|
);
|
|
558
539
|
return {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
540
|
+
version: rawConfig.version,
|
|
541
|
+
platforms: rawConfig.platforms ?? [...DEFAULT_PLATFORMS],
|
|
542
|
+
truthmark: {
|
|
543
|
+
workspace,
|
|
544
|
+
routes: {
|
|
545
|
+
index: DERIVED_TRUTHMARK_PATHS.routesIndex,
|
|
546
|
+
areas: DERIVED_TRUTHMARK_PATHS.routeAreasRoot,
|
|
547
|
+
defaultArea: DERIVED_TRUTHMARK_PATHS.defaultArea,
|
|
548
|
+
maxDelegationDepth: DERIVED_TRUTHMARK_PATHS.maxDelegationDepth
|
|
549
|
+
},
|
|
550
|
+
truth: {
|
|
551
|
+
productRoot: DERIVED_TRUTHMARK_PATHS.productTruthRoot,
|
|
552
|
+
engineeringRoot: DERIVED_TRUTHMARK_PATHS.engineeringTruthRoot
|
|
553
|
+
},
|
|
554
|
+
templates: {
|
|
555
|
+
root: DERIVED_TRUTHMARK_PATHS.templatesRoot
|
|
556
|
+
},
|
|
557
|
+
generated: {
|
|
558
|
+
portal: {
|
|
559
|
+
enabled: rawConfig.truthmark.generated.portal.enabled
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
paths: {
|
|
563
|
+
routesIndex,
|
|
564
|
+
routeAreasRoot,
|
|
565
|
+
productTruthRoot,
|
|
566
|
+
engineeringTruthRoot,
|
|
567
|
+
templatesRoot,
|
|
568
|
+
portalOutput,
|
|
569
|
+
portalTemplate
|
|
570
|
+
},
|
|
571
|
+
controlledPaths: [
|
|
572
|
+
routesIndex,
|
|
573
|
+
`${routeAreasRoot}/**/*.md`,
|
|
574
|
+
`${productTruthRoot}/**/*.md`,
|
|
575
|
+
`${engineeringTruthRoot}/**/*.md`,
|
|
576
|
+
`${templatesRoot}/*.md`
|
|
577
|
+
]
|
|
578
|
+
},
|
|
579
|
+
frontmatter: {
|
|
580
|
+
required: rawConfig.frontmatter?.required ?? [],
|
|
581
|
+
recommended: rawConfig.frontmatter?.recommended ?? []
|
|
582
|
+
},
|
|
583
|
+
ignore: rawConfig.ignore ?? []
|
|
593
584
|
};
|
|
594
585
|
};
|
|
595
|
-
var
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
return {
|
|
599
|
-
truthDocuments: [],
|
|
600
|
-
truthDocumentEntries: [],
|
|
601
|
-
diagnostics: []
|
|
602
|
-
};
|
|
586
|
+
var compatibilityDiagnostics = (rawConfig, configPath) => {
|
|
587
|
+
if (!("instruction_targets" in rawConfig)) {
|
|
588
|
+
return [];
|
|
603
589
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
590
|
+
return [
|
|
591
|
+
{
|
|
592
|
+
category: "config",
|
|
593
|
+
severity: "review",
|
|
594
|
+
message: "instruction_targets is accepted for compatibility but ignored; select platforms to control managed instruction-file writes.",
|
|
595
|
+
file: configPath
|
|
596
|
+
}
|
|
597
|
+
];
|
|
598
|
+
};
|
|
599
|
+
var loadConfig = async (rootDir) => {
|
|
600
|
+
const absolutePath = resolveRepoPath(rootDir, CONFIG_PATH);
|
|
601
|
+
let source;
|
|
602
|
+
try {
|
|
603
|
+
source = await fs2.readFile(absolutePath, "utf8");
|
|
604
|
+
} catch (error) {
|
|
605
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
606
|
+
return {
|
|
607
|
+
status: "missing",
|
|
608
|
+
config: null,
|
|
609
|
+
diagnostics: [
|
|
610
|
+
toConfigDiagnostic("Missing .truthmark/config.yml.", CONFIG_PATH)
|
|
611
|
+
],
|
|
612
|
+
configPath: CONFIG_PATH
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
throw error;
|
|
615
616
|
}
|
|
616
|
-
let
|
|
617
|
+
let parsedConfig;
|
|
617
618
|
try {
|
|
618
|
-
|
|
619
|
-
sectionLines.slice(
|
|
620
|
-
yamlFenceRange.openingFenceIndex + 1,
|
|
621
|
-
yamlFenceRange.closingFenceIndex
|
|
622
|
-
).join("\n")
|
|
623
|
-
);
|
|
619
|
+
parsedConfig = parse(source);
|
|
624
620
|
} catch (error) {
|
|
625
621
|
return {
|
|
626
|
-
|
|
627
|
-
|
|
622
|
+
status: "invalid",
|
|
623
|
+
config: null,
|
|
628
624
|
diagnostics: [
|
|
629
|
-
|
|
630
|
-
`
|
|
631
|
-
|
|
625
|
+
toConfigDiagnostic(
|
|
626
|
+
`Invalid YAML: ${error instanceof Error ? error.message : String(error)}`,
|
|
627
|
+
CONFIG_PATH
|
|
632
628
|
)
|
|
633
|
-
]
|
|
629
|
+
],
|
|
630
|
+
configPath: CONFIG_PATH
|
|
634
631
|
};
|
|
635
632
|
}
|
|
636
|
-
const
|
|
637
|
-
|
|
633
|
+
const unsupportedDiagnostics = unsupportedShapeDiagnostics(
|
|
634
|
+
parsedConfig,
|
|
635
|
+
CONFIG_PATH
|
|
636
|
+
);
|
|
637
|
+
if (unsupportedDiagnostics.length > 0) {
|
|
638
638
|
return {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
diagnostics:
|
|
642
|
-
|
|
643
|
-
`Area ${areaName} must define a truth_documents array inside the fenced YAML block.`,
|
|
644
|
-
areaName
|
|
645
|
-
)
|
|
646
|
-
]
|
|
639
|
+
status: "invalid",
|
|
640
|
+
config: null,
|
|
641
|
+
diagnostics: unsupportedDiagnostics,
|
|
642
|
+
configPath: CONFIG_PATH
|
|
647
643
|
};
|
|
648
644
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
realizes: parseStringListField(rawEntry, "realizes"),
|
|
676
|
-
dependsOn: parseStringListField(rawEntry, "depends_on")
|
|
677
|
-
});
|
|
645
|
+
if (!validateTruthmarkConfig(parsedConfig)) {
|
|
646
|
+
return {
|
|
647
|
+
status: "invalid",
|
|
648
|
+
config: null,
|
|
649
|
+
diagnostics: (validateTruthmarkConfig.errors ?? []).map(
|
|
650
|
+
(error) => {
|
|
651
|
+
const propertyPath = error.instancePath || "/";
|
|
652
|
+
const additionalProperty = error.keyword === "additionalProperties" && error.params && "additionalProperty" in error.params ? String(error.params.additionalProperty) : null;
|
|
653
|
+
const message = additionalProperty ? `${propertyPath} additional property ${additionalProperty} is not allowed` : `${propertyPath} ${error.message ?? "is invalid"}`.trim();
|
|
654
|
+
return toConfigDiagnostic(message, CONFIG_PATH);
|
|
655
|
+
}
|
|
656
|
+
),
|
|
657
|
+
configPath: CONFIG_PATH
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
const pathDiagnostics = validateWorkspacePaths(
|
|
661
|
+
parsedConfig,
|
|
662
|
+
CONFIG_PATH
|
|
663
|
+
);
|
|
664
|
+
if (pathDiagnostics.length > 0) {
|
|
665
|
+
return {
|
|
666
|
+
status: "invalid",
|
|
667
|
+
config: null,
|
|
668
|
+
diagnostics: pathDiagnostics,
|
|
669
|
+
configPath: CONFIG_PATH
|
|
670
|
+
};
|
|
678
671
|
}
|
|
679
672
|
return {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
diagnostics
|
|
673
|
+
status: "loaded",
|
|
674
|
+
config: normalizeConfig(parsedConfig),
|
|
675
|
+
diagnostics: compatibilityDiagnostics(
|
|
676
|
+
parsedConfig,
|
|
677
|
+
CONFIG_PATH
|
|
678
|
+
),
|
|
679
|
+
configPath: CONFIG_PATH
|
|
683
680
|
};
|
|
684
681
|
};
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
sectionLines,
|
|
692
|
-
areaName,
|
|
693
|
-
options
|
|
694
|
-
);
|
|
695
|
-
if (yamlResult.diagnostics.length > 0 || yamlFenceRange.closingFenceIndex === null) {
|
|
696
|
-
return yamlResult;
|
|
697
|
-
}
|
|
698
|
-
return yamlResult;
|
|
682
|
+
|
|
683
|
+
// src/config/render.ts
|
|
684
|
+
import { parse as parse2, parseDocument, stringify } from "yaml";
|
|
685
|
+
var normalizePlatforms = (platforms) => {
|
|
686
|
+
const selected = new Set(platforms);
|
|
687
|
+
return SUPPORTED_PLATFORMS.filter((platform) => selected.has(platform));
|
|
699
688
|
};
|
|
700
|
-
var
|
|
701
|
-
const
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
currentSections.get("Truth documents") ?? [],
|
|
716
|
-
currentAreaName,
|
|
717
|
-
options
|
|
718
|
-
);
|
|
719
|
-
const { truthDocuments, truthDocumentEntries } = truthDocumentResult;
|
|
720
|
-
const areaFiles = parseListSection(currentSections.get("Area files") ?? []);
|
|
721
|
-
const codeSurface = parseListSection(
|
|
722
|
-
currentSections.get("Code surface") ?? []
|
|
723
|
-
);
|
|
724
|
-
const updateTruthWhen = parseListSection(
|
|
725
|
-
currentSections.get("Update truth when") ?? []
|
|
726
|
-
);
|
|
727
|
-
const areaKey = slugify(currentAreaName);
|
|
728
|
-
const areaId = areaKey.length > 0 ? areaKey : `area-${areaIndex}`;
|
|
729
|
-
const hasTruthDocuments = truthDocuments.length > 0;
|
|
730
|
-
const hasAreaFiles = areaFiles.length > 0;
|
|
731
|
-
areaIndex += 1;
|
|
732
|
-
diagnostics.push(...truthDocumentResult.diagnostics);
|
|
733
|
-
if (hasTruthDocuments) {
|
|
734
|
-
truthDocumentReferences.push({
|
|
735
|
-
id: areaId,
|
|
736
|
-
name: currentAreaName,
|
|
737
|
-
key: areaKey,
|
|
738
|
-
truthDocuments,
|
|
739
|
-
truthDocumentEntries
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
if (hasTruthDocuments === hasAreaFiles || codeSurface.length === 0 || updateTruthWhen.length === 0) {
|
|
743
|
-
diagnostics.push(
|
|
744
|
-
createAreaDiagnostic(
|
|
745
|
-
`Area ${currentAreaName} must define exactly one of Truth documents or Area files, plus Code surface and Update truth when sections.`,
|
|
746
|
-
currentAreaName
|
|
747
|
-
)
|
|
748
|
-
);
|
|
749
|
-
} else if (hasAreaFiles) {
|
|
750
|
-
areaFileReferences.push({
|
|
751
|
-
id: areaId,
|
|
752
|
-
name: currentAreaName,
|
|
753
|
-
key: areaKey,
|
|
754
|
-
areaFiles,
|
|
755
|
-
codeSurface,
|
|
756
|
-
updateTruthWhen
|
|
757
|
-
});
|
|
758
|
-
} else {
|
|
759
|
-
areas.push({
|
|
760
|
-
id: areaId,
|
|
761
|
-
name: currentAreaName,
|
|
762
|
-
key: areaKey,
|
|
763
|
-
truthDocuments,
|
|
764
|
-
truthDocumentEntries,
|
|
765
|
-
codeSurface,
|
|
766
|
-
updateTruthWhen
|
|
767
|
-
});
|
|
768
|
-
}
|
|
769
|
-
currentAreaName = null;
|
|
770
|
-
currentSections = /* @__PURE__ */ new Map();
|
|
771
|
-
currentSectionName = null;
|
|
772
|
-
};
|
|
773
|
-
for (const line of lines) {
|
|
774
|
-
const areaHeadingMatch = line.match(/^\s{0,3}##\s+(.*)$/u);
|
|
775
|
-
if (areaHeadingMatch) {
|
|
776
|
-
const heading = areaHeadingMatch[1]?.trim() ?? null;
|
|
777
|
-
flushArea();
|
|
778
|
-
currentAreaName = heading === "Source References" ? null : heading;
|
|
779
|
-
continue;
|
|
780
|
-
}
|
|
781
|
-
if (!currentAreaName) {
|
|
782
|
-
continue;
|
|
783
|
-
}
|
|
784
|
-
if (/^(Truth documents|Area files|Code surface|Update truth when):$/u.test(
|
|
785
|
-
line.trim()
|
|
786
|
-
)) {
|
|
787
|
-
currentSectionName = line.trim().slice(0, -1);
|
|
788
|
-
currentSections.set(currentSectionName, []);
|
|
789
|
-
continue;
|
|
790
|
-
}
|
|
791
|
-
if (currentSectionName) {
|
|
792
|
-
currentSections.get(currentSectionName)?.push(line);
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
flushArea();
|
|
796
|
-
return {
|
|
797
|
-
areas,
|
|
798
|
-
truthDocumentReferences,
|
|
799
|
-
areaFileReferences,
|
|
800
|
-
diagnostics
|
|
801
|
-
};
|
|
689
|
+
var renderConfig = (platforms = []) => {
|
|
690
|
+
const normalized = normalizePlatforms(platforms);
|
|
691
|
+
const config = createDefaultRawConfig();
|
|
692
|
+
if (normalized.length > 0) config.platforms = normalized;
|
|
693
|
+
return stringify(config);
|
|
694
|
+
};
|
|
695
|
+
var updateConfigPlatforms = (source, platforms) => {
|
|
696
|
+
const normalized = normalizePlatforms(platforms);
|
|
697
|
+
const parsed = parse2(source);
|
|
698
|
+
if (JSON.stringify(normalizePlatforms(parsed.platforms ?? [])) === JSON.stringify(normalized))
|
|
699
|
+
return source;
|
|
700
|
+
const document = parseDocument(source);
|
|
701
|
+
if (normalized.length === 0) document.delete("platforms");
|
|
702
|
+
else document.set("platforms", normalized);
|
|
703
|
+
return document.toString();
|
|
802
704
|
};
|
|
803
705
|
|
|
804
706
|
// src/truth/docs.ts
|
|
@@ -812,1521 +714,1614 @@ var resolveEngineeringTruthRoot = (config) => {
|
|
|
812
714
|
return config.truthmark.paths.engineeringTruthRoot;
|
|
813
715
|
};
|
|
814
716
|
|
|
815
|
-
// src/
|
|
816
|
-
var
|
|
817
|
-
return
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
717
|
+
// src/truth/evidence.ts
|
|
718
|
+
var renderClaimEvidenceCheckedSection = (items) => {
|
|
719
|
+
return [
|
|
720
|
+
"Evidence checked:",
|
|
721
|
+
...items.map((item) => {
|
|
722
|
+
return [
|
|
723
|
+
`- Claim: ${item.claim}`,
|
|
724
|
+
` Evidence: ${item.evidence.join(" / ")}`,
|
|
725
|
+
` Result: ${item.result}`
|
|
726
|
+
].join("\n");
|
|
727
|
+
})
|
|
728
|
+
].join("\n");
|
|
822
729
|
};
|
|
823
|
-
var
|
|
824
|
-
var renderLaneRootReadmeSummary = (lane) => {
|
|
825
|
-
if (lane === "product") {
|
|
826
|
-
return [
|
|
827
|
-
"Product truth owns capability promises, boundaries, decisions, and acceptance criteria.",
|
|
828
|
-
"Product lane docs state what must be true, why it matters, and what success means."
|
|
829
|
-
].join(" ");
|
|
830
|
-
}
|
|
730
|
+
var renderAuditEvidenceCheckedSection = (items) => {
|
|
831
731
|
return [
|
|
832
|
-
"
|
|
833
|
-
|
|
834
|
-
|
|
732
|
+
"Evidence checked:",
|
|
733
|
+
...items.map((item) => {
|
|
734
|
+
return [
|
|
735
|
+
`- Finding: ${item.finding}`,
|
|
736
|
+
` Evidence: ${item.evidence.join(" / ")}`,
|
|
737
|
+
` Suggested fix: ${item.suggestedFix}`,
|
|
738
|
+
` Confidence: ${item.confidence}`
|
|
739
|
+
].join("\n");
|
|
740
|
+
})
|
|
741
|
+
].join("\n");
|
|
835
742
|
};
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
return
|
|
743
|
+
|
|
744
|
+
// src/agents/shared.ts
|
|
745
|
+
var renderBulletLine = (line) => {
|
|
746
|
+
const normalized = line.trim().replace(/^-\s*/u, "");
|
|
747
|
+
return `- ${normalized}`;
|
|
841
748
|
};
|
|
842
|
-
var
|
|
843
|
-
return
|
|
749
|
+
var renderBulletBlock = (lines, indent = " ") => {
|
|
750
|
+
return lines.split(/\n/u).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => `${indent}${renderBulletLine(line)}`).join("\n");
|
|
844
751
|
};
|
|
845
|
-
var
|
|
846
|
-
|
|
752
|
+
var renderLaneClassificationRuleBlock = (config = defaultAgentConfig(), indent = " ") => {
|
|
753
|
+
const [, ...rules] = renderLaneClassificationInstructions(config).split(/\n/u);
|
|
754
|
+
return renderBulletBlock(rules.join("\n"), indent);
|
|
847
755
|
};
|
|
848
|
-
var
|
|
849
|
-
const
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
756
|
+
var renderReadOnlyLaneClassificationRuleBlock = (config = defaultAgentConfig(), indent = " ") => {
|
|
757
|
+
const productTruthRoot = resolveProductTruthRoot(config);
|
|
758
|
+
const engineeringTruthRoot = resolveEngineeringTruthRoot(config);
|
|
759
|
+
return renderBulletBlock(
|
|
760
|
+
[
|
|
761
|
+
"classify the request or changed surface as product-lane, engineering-lane, both-lane, or ambiguous for reporting only",
|
|
762
|
+
`product-lane ownership belongs under ${productTruthRoot} and describes product promises, boundaries, rationale, decisions, and success criteria`,
|
|
763
|
+
`engineering-lane ownership belongs under ${engineeringTruthRoot} and describes source-backed current realization, contracts, architecture, workflows, operations, or tests`,
|
|
764
|
+
"both-lane ownership uses separate product and engineering docs cross-linked in route YAML with realized_by and realizes, not in doc frontmatter",
|
|
765
|
+
"ambiguous lane ownership should be reported for manual handoff or routed to Truth Structure",
|
|
766
|
+
LANE_INVARIANT
|
|
767
|
+
].join("\n"),
|
|
768
|
+
indent
|
|
855
769
|
);
|
|
856
|
-
return [
|
|
857
|
-
"---",
|
|
858
|
-
"status: active",
|
|
859
|
-
"doc_type: route-index",
|
|
860
|
-
`last_reviewed: ${currentDate()}`,
|
|
861
|
-
"---",
|
|
862
|
-
"",
|
|
863
|
-
"# Truthmark Areas",
|
|
864
|
-
"",
|
|
865
|
-
`## ${title}`,
|
|
866
|
-
"",
|
|
867
|
-
"Area files:",
|
|
868
|
-
`- ${childPath}`,
|
|
869
|
-
"",
|
|
870
|
-
"Code surface:",
|
|
871
|
-
"- src/**",
|
|
872
|
-
"",
|
|
873
|
-
"Update truth when:",
|
|
874
|
-
"- behavior changes affect the routed truth documents",
|
|
875
|
-
"- API contracts or current feature behavior changes",
|
|
876
|
-
"",
|
|
877
|
-
"## Source References",
|
|
878
|
-
"",
|
|
879
|
-
`- ${sourceOfTruth}`,
|
|
880
|
-
""
|
|
881
|
-
].join("\n");
|
|
882
770
|
};
|
|
883
|
-
var
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
);
|
|
771
|
+
var DECISION_TRUTH_INSTRUCTIONS = [
|
|
772
|
+
"Decision truth lives in the canonical doc it governs; date active decisions inline when added or changed.",
|
|
773
|
+
"Do not create separate active-decision ADR/planning logs; replace the active decision and let Git history carry the audit trail.",
|
|
774
|
+
"Product decisions belong in product truth; engineering, architecture, contract, workflow, and operational decisions belong in engineering truth."
|
|
775
|
+
].join("\n");
|
|
776
|
+
var LANE_INVARIANT = "Do not make product docs a summary of engineering docs. Do not make engineering docs a detailed version of product docs. Product truth says what must be true and why. Engineering truth says how the repository currently realizes it.";
|
|
777
|
+
var renderLaneClassificationInstructions = (config = defaultAgentConfig()) => {
|
|
778
|
+
const productTruthRoot = resolveProductTruthRoot(config);
|
|
779
|
+
const engineeringTruthRoot = resolveEngineeringTruthRoot(config);
|
|
893
780
|
return [
|
|
894
|
-
"
|
|
895
|
-
"
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
"
|
|
899
|
-
"",
|
|
900
|
-
|
|
901
|
-
"",
|
|
902
|
-
`## ${title}`,
|
|
903
|
-
"",
|
|
904
|
-
"Truth documents:",
|
|
905
|
-
"```yaml",
|
|
906
|
-
"truth_documents:",
|
|
907
|
-
` - path: ${bootstrapTruthDoc}`,
|
|
908
|
-
" kind: engineering-workflow",
|
|
909
|
-
" lane: engineering",
|
|
910
|
-
"```",
|
|
911
|
-
"",
|
|
912
|
-
"This is a provisional bootstrap route. It exists only to make fresh repositories routeable until real product, service, domain, or ownership areas are created.",
|
|
913
|
-
"",
|
|
914
|
-
"Code surface:",
|
|
915
|
-
"- src/**",
|
|
916
|
-
"",
|
|
917
|
-
"Update truth when:",
|
|
918
|
-
"- this provisional bootstrap route is the only match for a real code surface",
|
|
919
|
-
"- route ownership is still broad, mixed, or ambiguous",
|
|
920
|
-
"- Run Truth Structure before normal Truth Sync so the touched code gets a bounded owner",
|
|
921
|
-
"",
|
|
922
|
-
"## Source References",
|
|
923
|
-
"",
|
|
924
|
-
`- ${sourceOfTruth}`,
|
|
925
|
-
""
|
|
781
|
+
"Lane review questions:",
|
|
782
|
+
"- before writing canonical truth docs, classify the request or change as product-lane, engineering-lane, both-lane, or ambiguous",
|
|
783
|
+
`- product-lane writes belong under ${productTruthRoot} and state product promises, boundaries, rationale, decisions, and success criteria`,
|
|
784
|
+
`- engineering-lane writes belong under ${engineeringTruthRoot} and state source-backed current realization, contracts, architecture, workflows, operations, or tests`,
|
|
785
|
+
"- both-lane work must write separate product and engineering docs and cross-link them in route YAML with realized_by and realizes, not in doc frontmatter",
|
|
786
|
+
"- ambiguous lane ownership must stop or invoke Truth Structure instead of writing a mixed document",
|
|
787
|
+
`- ${LANE_INVARIANT}`
|
|
926
788
|
].join("\n");
|
|
927
789
|
};
|
|
928
|
-
var
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
790
|
+
var EVIDENCE_AUTHORITY_INSTRUCTIONS = [
|
|
791
|
+
"Repository instruction files and explicitly configured policy docs remain instruction authority when present; do not assume a repository uses any particular policy path.",
|
|
792
|
+
"Implementation code and canonical truth docs are inspected evidence for current behavior; they do not silently override workflow write boundaries."
|
|
793
|
+
].join("\n");
|
|
794
|
+
var REPOSITORY_INTELLIGENCE_INSTRUCTIONS = [
|
|
795
|
+
"Repository intelligence artifacts are optional derived context: RepoIndex, RouteMap, ImpactSet, and WorkflowState/action context may guide routing, write boundaries, and verification planning when available.",
|
|
796
|
+
"They do not override checkout evidence, canonical truth docs, route files, or workflow write boundaries.",
|
|
797
|
+
"If unavailable, inspect any present Truthmark config, route files, source files, truth docs, and tests directly, then report that repository-intelligence artifacts were not generated."
|
|
798
|
+
].join("\n");
|
|
799
|
+
var FEATURE_DOC_TEMPLATE_INSTRUCTIONS = [
|
|
800
|
+
"When creating or updating a truth doc, inspect the routed truth kind and use the matching template under the configured Truthmark templates root.",
|
|
801
|
+
"Supported kinds: product-capability, engineering-behavior, engineering-contract, engineering-architecture, engineering-workflow, engineering-operations, and engineering-test-behavior.",
|
|
802
|
+
"Treat the HTML comments under each template section as normative authoring guidance for that section.",
|
|
803
|
+
"Align existing docs to that template and write or repair section content so it satisfies the comment guidance while preserving accurate authored content.",
|
|
804
|
+
"If the template is missing, use lane-specific sections: product truth says what must be true and why; engineering truth says how the repository currently realizes it.",
|
|
805
|
+
"Teams may edit template files under the configured Truthmark templates root to define their local truth-doc standards."
|
|
806
|
+
].join("\n");
|
|
807
|
+
var TRUTH_DOC_AUTHORING_STYLE_INSTRUCTIONS = [
|
|
808
|
+
"Truth-doc prose style:",
|
|
809
|
+
"- Use professional, plain technical prose. Prefer specific current-state claims over promotional, symbolic, or generic significance language.",
|
|
810
|
+
"- Avoid common AI-writing tells: pivotal, crucial, underscores, serves as, stands as, showcases, landscape, vague expert attributions, and generic upbeat conclusions.",
|
|
811
|
+
"- Keep claims evidence-backed and diff-friendly: one durable claim per bullet or line; paragraphs should be no longer than one or two short sentences.",
|
|
812
|
+
"- Do not add personality, rhetorical flourish, first-person commentary, or marketing tone.",
|
|
813
|
+
"- Rewrite dense or formulaic prose only when it improves readability without removing scope, evidence, decisions, or source references."
|
|
814
|
+
].join("\n");
|
|
815
|
+
var renderTruthDocOwnershipGateSection = (subject, outcome) => {
|
|
939
816
|
return [
|
|
940
|
-
"
|
|
941
|
-
|
|
942
|
-
"
|
|
943
|
-
|
|
944
|
-
"
|
|
945
|
-
"",
|
|
946
|
-
`# ${title} Bootstrap Routing`,
|
|
947
|
-
"",
|
|
948
|
-
"## Purpose",
|
|
949
|
-
"",
|
|
950
|
-
`This doc records the provisional broad route for ${defaultArea}.`,
|
|
951
|
-
"This doc is a bootstrap handoff, not a behavior truth dumping ground.",
|
|
952
|
-
"It is not a substitute for bounded product and engineering truth docs.",
|
|
953
|
-
"",
|
|
954
|
-
"## Scope",
|
|
955
|
-
"",
|
|
956
|
-
"This doc owns only the initial routing workflow for a fresh Truthmark repository whose default route still maps a broad code surface such as `src/**`.",
|
|
957
|
-
"It does not own implementation behavior under that code surface.",
|
|
958
|
-
"",
|
|
959
|
-
"## Current Implementation Behavior",
|
|
960
|
-
"",
|
|
961
|
-
"The scaffold creates this provisional bootstrap handoff only when a default broad route needs a canonical owner. Agents use it as a signal to run Truth Structure and create bounded routes before normal Truth Sync, not as a place to accumulate implementation claims.",
|
|
962
|
-
"",
|
|
963
|
-
"## Product Truth Links",
|
|
964
|
-
"",
|
|
965
|
-
"- None. This is an engineering bootstrap handoff for routing setup, not a product promise.",
|
|
966
|
-
"",
|
|
967
|
-
"## Triggers",
|
|
968
|
-
"",
|
|
969
|
-
"- A real code change maps only to this provisional broad route.",
|
|
970
|
-
"- Truth Sync cannot identify a specific behavior-owned route and bounded truth owner.",
|
|
971
|
-
"- A maintainer or agent is onboarding the first real product, service, domain, package, or ownership area.",
|
|
972
|
-
"",
|
|
973
|
-
"## Inputs",
|
|
974
|
-
"",
|
|
975
|
-
"- Current route files under the configured Truthmark route root.",
|
|
976
|
-
"- The touched code, tests, configuration, and existing docs needed to infer the smallest real owner.",
|
|
977
|
-
"- Repository instruction files that exist in the checkout.",
|
|
978
|
-
"",
|
|
979
|
-
"## Execution Model",
|
|
980
|
-
"",
|
|
981
|
-
"Run Truth Structure before normal Truth Sync when real code changes touch only this broad route. Truth Structure should create or repair bounded areas first; Truth Sync should then update the bounded owner docs.",
|
|
982
|
-
"",
|
|
983
|
-
"## Steps",
|
|
984
|
-
"",
|
|
985
|
-
"1. Treat this route as provisional and insufficient for normal behavior maintenance.",
|
|
986
|
-
"2. Inspect the touched code/test surface and infer the narrowest durable owner.",
|
|
987
|
-
"3. Create or repair route entries and truth docs for that owner.",
|
|
988
|
-
"4. Leave this bootstrap doc small; do not append behavior details here.",
|
|
989
|
-
"5. Resume Truth Sync only after the touched code resolves to a bounded owner.",
|
|
990
|
-
"",
|
|
991
|
-
"## State, Retry, And Failure Behavior",
|
|
992
|
-
"",
|
|
993
|
-
"If ownership cannot be inferred safely, stop and report manual-review files instead of widening this route or adding generic behavior prose.",
|
|
994
|
-
"",
|
|
995
|
-
"## Outputs",
|
|
996
|
-
"",
|
|
997
|
-
"- Bounded route areas and lane-appropriate truth docs for the touched surface.",
|
|
998
|
-
"- A compact manual handoff report when ownership remains ambiguous.",
|
|
999
|
-
"",
|
|
1000
|
-
"## Engineering Decisions",
|
|
1001
|
-
"",
|
|
1002
|
-
`- Decision (${today}): Default broad routing is provisional bootstrap state. Agents should create bounded areas before normal Truth Sync rather than extending a catch-all overview doc.`,
|
|
1003
|
-
"",
|
|
1004
|
-
"## Rationale",
|
|
1005
|
-
"",
|
|
1006
|
-
"Scoped ownership keeps agent context close to affected files and prevents broad default docs from absorbing unrelated behavior. This preserves agent-native truth maintenance without adding a token-heavy discovery layer.",
|
|
1007
|
-
"",
|
|
1008
|
-
"## Non-Goals",
|
|
1009
|
-
"",
|
|
1010
|
-
"- This doc is not a repository behavior overview.",
|
|
1011
|
-
"- This doc is not a product capability or engineering behavior owner.",
|
|
1012
|
-
"- This doc is not a permanent home for claims about files under `src/**`.",
|
|
1013
|
-
"",
|
|
1014
|
-
"## Maintenance Notes",
|
|
1015
|
-
"",
|
|
1016
|
-
"Keep this doc short. When a repository has real bounded routes, prefer updating those routes and their truth docs instead of expanding this bootstrap handoff.",
|
|
1017
|
-
"",
|
|
1018
|
-
"## Source References",
|
|
1019
|
-
"",
|
|
1020
|
-
`- ${routeSource}`,
|
|
1021
|
-
`- ${configSource}`,
|
|
1022
|
-
""
|
|
817
|
+
"Truth-doc ownership review:",
|
|
818
|
+
`- before editing or relying on ${subject}, verify each target/source truth doc is a bounded owner for the behavior`,
|
|
819
|
+
"- if a target/source doc mixes independent owners, spans unrelated behaviors, acts as an index, or needs cross-owner edits, do not patch or in-place repair it",
|
|
820
|
+
`- ${outcome}`,
|
|
821
|
+
"- report Ownership reviewed, Structure required, Truth docs split, Truth docs restructured, or Manual handoff reason as applicable"
|
|
1023
822
|
].join("\n");
|
|
1024
823
|
};
|
|
1025
|
-
var
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
824
|
+
var TRUTH_DOC_DECISION_RATIONALE_PRESERVATION_INSTRUCTIONS = [
|
|
825
|
+
"Decision/Rationale preservation review:",
|
|
826
|
+
"- before any truth-doc split, restructure, or shape repair, inventory existing Product Decisions, Engineering Decisions, and Rationale sections in every source or touched truth doc",
|
|
827
|
+
"- preserve each current decision and rationale in the correct product or engineering lane owner; when splitting, move it to the new owner doc rather than deleting it or leaving it in an index",
|
|
828
|
+
"- remove or narrow a decision or rationale only when checkout evidence shows it is stale or unsupported, and report the exact claim, evidence, and result",
|
|
829
|
+
"- if ownership of a decision or rationale is unclear, stop with manual-review files instead of deleting it or guessing",
|
|
830
|
+
"- after the edit, verify every touched truth doc keeps lane-appropriate decision/rationale sections and every pre-existing entry is preserved, moved, narrowed, removed with evidence, or blocked"
|
|
831
|
+
].join("\n");
|
|
832
|
+
var renderTruthDocRestructureGateSection = (scope) => {
|
|
1031
833
|
return [
|
|
1032
|
-
"
|
|
1033
|
-
|
|
1034
|
-
"
|
|
1035
|
-
|
|
1036
|
-
"
|
|
1037
|
-
"",
|
|
1038
|
-
"
|
|
1039
|
-
"",
|
|
1040
|
-
"This directory is an index for current truth docs organized by the configured Truthmark hierarchy.",
|
|
1041
|
-
"",
|
|
1042
|
-
renderLaneRootReadmeSummary(lane),
|
|
1043
|
-
"",
|
|
1044
|
-
renderLaneRootLeafDocGuidance(lane),
|
|
1045
|
-
"",
|
|
1046
|
-
"## Source References",
|
|
1047
|
-
"",
|
|
1048
|
-
`- ${sourceOfTruth}`,
|
|
1049
|
-
""
|
|
834
|
+
"Truth-doc shape repair review:",
|
|
835
|
+
`- ${scope}`,
|
|
836
|
+
"- repair shape in place only after the ownership review confirms the doc is the right bounded owner",
|
|
837
|
+
"- use Truth Structure for ownership splits; do not treat broad or mixed-owner docs as in-place repair work",
|
|
838
|
+
"- repair shape when a narrow edit would make truth worse: missing template sections, stale evidence conflicts, cross-section updates within one owner, or wrong frontmatter/source/headings",
|
|
839
|
+
"- preserve supported claims; remove, narrow, or record unsupported or stale claims for manual handoff",
|
|
840
|
+
"- report docs restructured and why a narrow edit was not sufficient"
|
|
1050
841
|
].join("\n");
|
|
1051
842
|
};
|
|
1052
|
-
var
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
templatePath,
|
|
1058
|
-
`${config.truthmark.paths.routeAreasRoot}/${defaultArea}.md`
|
|
1059
|
-
);
|
|
843
|
+
var ARCHITECTURE_DOC_BOUNDARY_INSTRUCTIONS = [
|
|
844
|
+
"Maintain architecture docs only for structure-level changes: system structure, module boundaries, runtime topology, persistence boundaries, cross-cutting contracts, or generated-surface ownership.",
|
|
845
|
+
"Keep ordinary behavior, endpoints, UI copy, validation rules, and bug fixes in behavior or contract docs unless they change those boundaries."
|
|
846
|
+
].join("\n");
|
|
847
|
+
var renderRouteFirstEvidenceGateSection = (subject, noImpactedDocOutcome) => {
|
|
1060
848
|
return [
|
|
1061
|
-
"
|
|
1062
|
-
|
|
1063
|
-
"
|
|
1064
|
-
|
|
1065
|
-
"
|
|
1066
|
-
"",
|
|
1067
|
-
|
|
1068
|
-
"",
|
|
1069
|
-
`This directory indexes bounded ${title.toLowerCase()} truth docs.`,
|
|
1070
|
-
"",
|
|
1071
|
-
"README.md files are indexes, not Truth Sync targets. Keep bounded truth in leaf docs in this directory.",
|
|
1072
|
-
"",
|
|
1073
|
-
"Current leaf docs:",
|
|
1074
|
-
"",
|
|
1075
|
-
"- [Bootstrap routing](bootstrap-routing.md)",
|
|
1076
|
-
"",
|
|
1077
|
-
"## Source References",
|
|
1078
|
-
"",
|
|
1079
|
-
`- ${sourceOfTruth}`,
|
|
1080
|
-
""
|
|
849
|
+
"Evidence checklist:",
|
|
850
|
+
`- route-first: map ${subject} to bounded route owners and primary canonical docs`,
|
|
851
|
+
"- review new or changed behavior-bearing claims only in touched docs, route ownership, lane-specific decisions, and rationale",
|
|
852
|
+
"- support claims with primary checkout evidence: implementation, config, routing, generated templates, schemas, or contract definitions",
|
|
853
|
+
"- tests/examples/canonical docs corroborate; they are not sole proof when implementation conflicts",
|
|
854
|
+
"- remove, narrow, or record unsupported claims for manual handoff",
|
|
855
|
+
`- ${noImpactedDocOutcome}`
|
|
1081
856
|
].join("\n");
|
|
1082
857
|
};
|
|
1083
|
-
var
|
|
858
|
+
var renderTopologyEvidenceGateSection = () => {
|
|
1084
859
|
return [
|
|
1085
|
-
|
|
1086
|
-
"",
|
|
1087
|
-
"
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
"",
|
|
1091
|
-
`{{${section.placeholder}}}`,
|
|
1092
|
-
""
|
|
1093
|
-
];
|
|
1094
|
-
};
|
|
1095
|
-
var titleToPlaceholder = (title) => {
|
|
1096
|
-
return title.replace(/^#+\s+/u, "").toLowerCase().replaceAll(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
860
|
+
"Evidence checklist:",
|
|
861
|
+
"- apply the evidence checklist before finishing when Truth Structure writes routed docs, ownership claims, lane-specific decisions, or rationale",
|
|
862
|
+
"- support ownership/behavior claims with topology or primary checkout evidence from layout, implementation boundaries, docs, config, route files, tests, templates, schemas, or contracts",
|
|
863
|
+
"- tests/examples/canonical docs corroborate; remove, narrow, or record unsupported claims for manual handoff"
|
|
864
|
+
].join("\n");
|
|
1097
865
|
};
|
|
1098
|
-
var
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
continue;
|
|
1106
|
-
}
|
|
1107
|
-
const line = rawLine.replace(/\r?\n$/u, "");
|
|
1108
|
-
const fenceMatch = /^(?: {0,3})(`{3,}|~{3,})/u.exec(line);
|
|
1109
|
-
if (fenceMatch) {
|
|
1110
|
-
const marker = fenceMatch[1]?.[0];
|
|
1111
|
-
const length = fenceMatch[1]?.length ?? 0;
|
|
1112
|
-
if (fencedCodeMarker === null) {
|
|
1113
|
-
fencedCodeMarker = marker;
|
|
1114
|
-
fencedCodeLength = length;
|
|
1115
|
-
} else if (marker === fencedCodeMarker && length >= fencedCodeLength) {
|
|
1116
|
-
fencedCodeMarker = null;
|
|
1117
|
-
fencedCodeLength = 0;
|
|
1118
|
-
}
|
|
1119
|
-
continue;
|
|
1120
|
-
}
|
|
1121
|
-
if (fencedCodeMarker === null && /^## .+$/u.test(line)) {
|
|
1122
|
-
matches.push({ heading: line.trim(), index: lineMatch.index });
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
return matches;
|
|
866
|
+
var renderAuditEvidenceGateSection = () => {
|
|
867
|
+
return [
|
|
868
|
+
"Evidence checklist:",
|
|
869
|
+
"- support each finding and suggested fix with evidence from config, route files, canonical docs, implementation, templates, or tests",
|
|
870
|
+
"- canonical docs are context, not sole proof when implementation conflicts",
|
|
871
|
+
"- remove unsupported findings or mark open questions; validate changed claims if you edit docs"
|
|
872
|
+
].join("\n");
|
|
1126
873
|
};
|
|
1127
|
-
var
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
874
|
+
var renderCodexSubagentModeSection = (agents, parentRule, writeAgents = []) => {
|
|
875
|
+
const writeAgentLines = writeAgents.length > 0 ? [
|
|
876
|
+
`- dispatch write-capable project agents only with explicit write leases: ${writeAgents.join(", ")}`,
|
|
877
|
+
"- each write lease must name objective, required reads, allowed writes, forbidden writes, evidence, verification, and report fields",
|
|
878
|
+
"- write workers must stop when a required edit is off-lease and report status, filesChanged, evidence, offLeaseChanges, blockers, and notes",
|
|
879
|
+
"- parent must inspect the actual checkout diff against each lease before accepting a worker report"
|
|
880
|
+
] : [];
|
|
881
|
+
const readOnlyScope = writeAgents.length > 0 ? "for verification" : "only";
|
|
882
|
+
const readOnlyWorkerLabel = writeAgents.length > 0 ? "read-only workers" : "workers";
|
|
883
|
+
return [
|
|
884
|
+
"Codex subagent mode:",
|
|
885
|
+
"- use automatically when this workflow runs in Codex and the parent agent chooses bounded subagent fan-out",
|
|
886
|
+
`- dispatch read-only project agents ${readOnlyScope}: ${agents.join(", ")}`,
|
|
887
|
+
`- ${readOnlyWorkerLabel} inspect checkout evidence directly, return structured findings, and must not edit files`,
|
|
888
|
+
`- parent supplies bounded evidence shards; ${readOnlyWorkerLabel} must not preload host instruction files or repo-wide policy docs unless assigned as evidence`,
|
|
889
|
+
...writeAgentLines,
|
|
890
|
+
`- ${parentRule}`
|
|
891
|
+
].join("\n");
|
|
1145
892
|
};
|
|
1146
|
-
var
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
893
|
+
var renderOpenCodeSubagentModeSection = (agents, parentRule, writeAgents = []) => {
|
|
894
|
+
const mentions = agents.map((agent) => `@${agent.replace(/_/gu, "-")}`);
|
|
895
|
+
const writeMentions = writeAgents.map(
|
|
896
|
+
(agent) => `@${agent.replace(/_/gu, "-")}`
|
|
897
|
+
);
|
|
898
|
+
const writeAgentLines = writeMentions.length > 0 ? [
|
|
899
|
+
`- dispatch write-capable project subagents only with explicit write leases: ${writeMentions.join(", ")}`,
|
|
900
|
+
"- each write lease must name objective, required reads, allowed writes, forbidden writes, evidence, verification, and report fields",
|
|
901
|
+
"- write workers must stop when a required edit is off-lease and report status, filesChanged, evidence, offLeaseChanges, blockers, and notes",
|
|
902
|
+
"- parent must inspect the actual checkout diff against each lease before accepting a worker report"
|
|
903
|
+
] : [];
|
|
904
|
+
const readOnlyScope = writeAgents.length > 0 ? "for verification" : "only";
|
|
905
|
+
const readOnlyWorkerLabel = writeAgents.length > 0 ? "read-only workers" : "workers";
|
|
906
|
+
return [
|
|
907
|
+
"OpenCode subagent mode:",
|
|
908
|
+
"- use automatically when this workflow runs in OpenCode and the parent agent chooses bounded subagent fan-out",
|
|
909
|
+
`- dispatch read-only project subagents ${readOnlyScope}: ${mentions.join(", ")}`,
|
|
910
|
+
`- ${readOnlyWorkerLabel} inspect checkout evidence directly, return structured findings, and must not edit files`,
|
|
911
|
+
`- parent supplies bounded evidence shards; ${readOnlyWorkerLabel} must not preload host instruction files or repo-wide policy docs unless assigned as evidence`,
|
|
912
|
+
...writeAgentLines,
|
|
913
|
+
`- ${parentRule}`
|
|
914
|
+
].join("\n");
|
|
1152
915
|
};
|
|
1153
|
-
var
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
}
|
|
1157
|
-
const lines = preamble.split("\n");
|
|
1158
|
-
const closingIndex = lines.findIndex(
|
|
1159
|
-
(line, index) => index > 0 && line.trim() === "---"
|
|
916
|
+
var renderClaudeSubagentModeSection = (agents, parentRule, writeAgents = []) => {
|
|
917
|
+
const mentions = agents.map(
|
|
918
|
+
(agent) => `${agent.replace(/_/gu, "-")} subagent`
|
|
1160
919
|
);
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
const
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
if (!skippingManagedField) {
|
|
1173
|
-
keptFrontmatterLines.push(line);
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
920
|
+
const writeMentions = writeAgents.map(
|
|
921
|
+
(agent) => `${agent.replace(/_/gu, "-")} subagent`
|
|
922
|
+
);
|
|
923
|
+
const writeAgentLines = writeMentions.length > 0 ? [
|
|
924
|
+
`- dispatch write-capable project subagents only with explicit write leases: ${writeMentions.join(", ")}`,
|
|
925
|
+
"- each write lease must name objective, required reads, allowed writes, forbidden writes, evidence, verification, and report fields",
|
|
926
|
+
"- write workers must stop when a required edit is off-lease and report status, filesChanged, evidence, offLeaseChanges, blockers, and notes",
|
|
927
|
+
"- parent must inspect the actual checkout diff against each lease before accepting a worker report"
|
|
928
|
+
] : [];
|
|
929
|
+
const readOnlyScope = writeAgents.length > 0 ? "for verification" : "only";
|
|
930
|
+
const readOnlySubagentLabel = writeAgents.length > 0 ? "read-only subagents" : "subagents";
|
|
1176
931
|
return [
|
|
1177
|
-
"
|
|
1178
|
-
|
|
1179
|
-
"
|
|
1180
|
-
|
|
1181
|
-
|
|
932
|
+
"Claude Code subagent mode:",
|
|
933
|
+
"- use automatically when this workflow runs in Claude Code and the parent agent chooses bounded subagent fan-out",
|
|
934
|
+
`- dispatch read-only project subagents ${readOnlyScope}: ${mentions.join(", ")}`,
|
|
935
|
+
`- ${readOnlySubagentLabel} inspect checkout evidence directly, return structured findings, and must not edit files`,
|
|
936
|
+
`- parent supplies bounded evidence shards; ${readOnlySubagentLabel} must not preload host instruction files or repo-wide policy docs unless assigned as evidence`,
|
|
937
|
+
...writeAgentLines,
|
|
938
|
+
`- ${parentRule}`
|
|
939
|
+
].join("\n");
|
|
1182
940
|
};
|
|
1183
|
-
var
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
const defaultParsed = parseTemplateSections(defaultTemplate);
|
|
1188
|
-
const existingParsed = parseTemplateSections(existingTemplate);
|
|
1189
|
-
const defaultHeadings = new Set(
|
|
1190
|
-
defaultParsed.sections.map((section) => section.heading)
|
|
941
|
+
var renderCopilotCustomAgentModeSection = (agents, parentRule, writeAgents = []) => {
|
|
942
|
+
const mentions = agents.map((agent) => `@${agent.replace(/_/gu, "-")}`);
|
|
943
|
+
const writeMentions = writeAgents.map(
|
|
944
|
+
(agent) => `@${agent.replace(/_/gu, "-")}`
|
|
1191
945
|
);
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
);
|
|
1201
|
-
if (nextDefaultSection) {
|
|
1202
|
-
const nextDefaultHeading = resolveManagedTemplateHeading(
|
|
1203
|
-
nextDefaultSection.heading
|
|
1204
|
-
);
|
|
1205
|
-
const bucket = customBeforeDefault.get(nextDefaultHeading) ?? [];
|
|
1206
|
-
bucket.push(section);
|
|
1207
|
-
customBeforeDefault.set(nextDefaultHeading, bucket);
|
|
1208
|
-
return;
|
|
1209
|
-
}
|
|
1210
|
-
trailingCustomSections.push(section);
|
|
1211
|
-
});
|
|
1212
|
-
const mergedSections = defaultParsed.sections.flatMap((section) => [
|
|
1213
|
-
...customBeforeDefault.get(section.heading) ?? [],
|
|
1214
|
-
section
|
|
1215
|
-
]);
|
|
946
|
+
const writeAgentLines = writeMentions.length > 0 ? [
|
|
947
|
+
`- dispatch write-capable project custom agents only with explicit write leases: ${writeMentions.join(", ")}`,
|
|
948
|
+
"- each write lease must name objective, required reads, allowed writes, forbidden writes, evidence, verification, and report fields",
|
|
949
|
+
"- write workers must stop when a required edit is off-lease and report status, filesChanged, evidence, offLeaseChanges, blockers, and notes",
|
|
950
|
+
"- parent must inspect the actual checkout diff against each lease before accepting a worker report"
|
|
951
|
+
] : [];
|
|
952
|
+
const readOnlyScope = writeAgents.length > 0 ? "for verification" : "only";
|
|
953
|
+
const readOnlyCustomAgentLabel = writeAgents.length > 0 ? "read-only custom agents" : "custom agents";
|
|
1216
954
|
return [
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
955
|
+
"Copilot custom-agent mode:",
|
|
956
|
+
"- use automatically when this workflow runs in Copilot and the parent agent chooses bounded custom-agent fan-out",
|
|
957
|
+
`- dispatch read-only project custom agents ${readOnlyScope}: ${mentions.join(", ")}`,
|
|
958
|
+
`- ${readOnlyCustomAgentLabel} inspect checkout evidence directly, return structured findings, and must not edit files`,
|
|
959
|
+
`- parent supplies bounded evidence shards; ${readOnlyCustomAgentLabel} must not preload host instruction files or repo-wide policy docs unless assigned as evidence`,
|
|
960
|
+
...writeAgentLines,
|
|
961
|
+
`- ${parentRule}`
|
|
962
|
+
].join("\n");
|
|
1222
963
|
};
|
|
1223
|
-
var
|
|
964
|
+
var defaultAgentConfig = () => {
|
|
965
|
+
return createDefaultConfig();
|
|
966
|
+
};
|
|
967
|
+
var renderHierarchySummary = (config) => {
|
|
968
|
+
const productRoot = resolveProductTruthRoot(config);
|
|
969
|
+
const engineeringRoot = resolveEngineeringTruthRoot(config);
|
|
1224
970
|
return [
|
|
1225
|
-
"
|
|
1226
|
-
"
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
"
|
|
1250
|
-
"-->",
|
|
1251
|
-
"",
|
|
1252
|
-
"{{scope}}",
|
|
1253
|
-
"",
|
|
1254
|
-
"This doc was created from the editable engineering-behavior template at {{template_path}}.",
|
|
1255
|
-
"",
|
|
1256
|
-
"## Current Implementation Behavior",
|
|
1257
|
-
"",
|
|
1258
|
-
"<!--",
|
|
1259
|
-
"Describe only current implemented behavior in present tense.",
|
|
1260
|
-
"Cover observable behavior, important defaults, and user/system-visible effects; exclude desired future behavior and speculative design.",
|
|
1261
|
-
"Every non-obvious claim should be checkable from Source References.",
|
|
1262
|
-
"-->",
|
|
1263
|
-
"",
|
|
1264
|
-
"{{current_implementation_behavior}}",
|
|
1265
|
-
"",
|
|
1266
|
-
"## Core Rules",
|
|
1267
|
-
"",
|
|
1268
|
-
"<!--",
|
|
1269
|
-
"Capture stable business rules, invariants, precedence rules, validation rules, and must-never constraints.",
|
|
1270
|
-
"Separate rules from incidental implementation details; cite current implementation or tests for rule enforcement.",
|
|
1271
|
-
"-->",
|
|
1272
|
-
"",
|
|
1273
|
-
"{{core_rules}}",
|
|
1274
|
-
"",
|
|
1275
|
-
"## Behavior Scenarios",
|
|
1276
|
-
"",
|
|
1277
|
-
"<!--",
|
|
1278
|
-
"Use compact scenario blocks only where they clarify normal, fallback, or compatibility-critical behavior.",
|
|
1279
|
-
"Write scenarios as current truth, not desired requirements: `#### Scenario: <implemented case>` followed by `- **GIVEN** ...`, `- **WHEN** ...`, `- **THEN** ...`, and optional `- **AND** ...` bullets.",
|
|
1280
|
-
"Keep each bullet evidence-backed and observable; do not force a scenario for every rule.",
|
|
1281
|
-
"-->",
|
|
1282
|
-
"",
|
|
1283
|
-
"{{behavior_scenarios}}",
|
|
971
|
+
"Truthmark hierarchy hints:",
|
|
972
|
+
"- Config, when present: .truthmark/config.yml",
|
|
973
|
+
`- Root route index, when present: ${config.truthmark.paths.routesIndex}`,
|
|
974
|
+
`- Area route files, when present: ${config.truthmark.paths.routeAreasRoot}/**/*.md`,
|
|
975
|
+
`- Product truth docs, when present: ${productRoot}/**/*.md`,
|
|
976
|
+
`- Engineering truth docs, when present: ${engineeringRoot}/**/*.md`
|
|
977
|
+
].join("\n");
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
// src/templates/agents-block.ts
|
|
981
|
+
var TRUTHMARK_BLOCK_START = "<!-- truthmark:start -->";
|
|
982
|
+
var TRUTHMARK_BLOCK_END = "<!-- truthmark:end -->";
|
|
983
|
+
var renderCompactHierarchySummary = (config) => {
|
|
984
|
+
const productTruthRoot = resolveProductTruthRoot(config);
|
|
985
|
+
const engineeringTruthRoot = resolveEngineeringTruthRoot(config);
|
|
986
|
+
const truthDocRoots = Array.from(
|
|
987
|
+
/* @__PURE__ */ new Set([productTruthRoot, engineeringTruthRoot])
|
|
988
|
+
).map((truthRoot3) => `${truthRoot3}/**/*.md`);
|
|
989
|
+
return `Hierarchy hints: config .truthmark/config.yml when present; routes ${config.truthmark.paths.routesIndex} and ${config.truthmark.paths.routeAreasRoot}/**/*.md when present; Truth docs: ${truthDocRoots.join(" and ")} when present.`;
|
|
990
|
+
};
|
|
991
|
+
var renderAgentsBlock = (config = defaultAgentConfig()) => {
|
|
992
|
+
const portalLine = config.truthmark.generated.portal.enabled ? `Truthmark Portal is a separate manual-only presentation workflow. Run it only when explicitly requested; it writes generated non-canonical static files under ${config.truthmark.paths.portalOutput}/. Markdown remains canonical.` : null;
|
|
993
|
+
return [
|
|
994
|
+
TRUTHMARK_BLOCK_START,
|
|
995
|
+
"## Truthmark Workflow",
|
|
1284
996
|
"",
|
|
1285
|
-
"
|
|
1286
|
-
|
|
1287
|
-
"
|
|
1288
|
-
"
|
|
1289
|
-
"
|
|
1290
|
-
"
|
|
1291
|
-
"",
|
|
1292
|
-
"
|
|
1293
|
-
"",
|
|
1294
|
-
|
|
1295
|
-
"",
|
|
1296
|
-
|
|
1297
|
-
"Capture user-visible or integration contracts: CLI/API shape, inputs, outputs, diagnostics, files, events, permissions, or links to canonical contract docs.",
|
|
1298
|
-
"Avoid duplicating a separate canonical contract doc; link to it when contract ownership lives elsewhere.",
|
|
1299
|
-
"-->",
|
|
1300
|
-
"",
|
|
1301
|
-
"{{contracts}}",
|
|
1302
|
-
"",
|
|
1303
|
-
"## Product Truth Links",
|
|
1304
|
-
"",
|
|
1305
|
-
"<!--",
|
|
1306
|
-
"List product truth docs this engineering doc realizes; author canonical realizes links in route YAML, not doc frontmatter.",
|
|
1307
|
-
"Use 'None.' when this is purely internal engineering behavior.",
|
|
1308
|
-
"-->",
|
|
1309
|
-
"",
|
|
1310
|
-
"{{product_truth_links}}",
|
|
1311
|
-
"",
|
|
1312
|
-
"## Engineering Decisions",
|
|
1313
|
-
"",
|
|
1314
|
-
"<!--",
|
|
1315
|
-
"Keep active decisions only, dated inline when added or changed.",
|
|
1316
|
-
"Explain decisions that shape behavior, boundaries, rejected alternatives, or migration constraints; replace stale decisions instead of appending historical logs.",
|
|
1317
|
-
"-->",
|
|
1318
|
-
"",
|
|
1319
|
-
"{{engineering_decisions}}",
|
|
1320
|
-
"",
|
|
1321
|
-
"## Rationale",
|
|
1322
|
-
"",
|
|
1323
|
-
"<!--",
|
|
1324
|
-
"Explain why the current behavior and active decisions are this way, including tradeoffs and constraints.",
|
|
1325
|
-
"Tie rationale to evidence-backed behavior; do not use this as a changelog.",
|
|
1326
|
-
"-->",
|
|
1327
|
-
"",
|
|
1328
|
-
"{{rationale}}",
|
|
1329
|
-
"",
|
|
1330
|
-
"## Non-Goals",
|
|
1331
|
-
"",
|
|
1332
|
-
"<!--",
|
|
1333
|
-
"Name adjacent behavior this doc intentionally does not own, especially tempting future expansions or neighboring route owners.",
|
|
1334
|
-
"Use this section to prevent scope creep and duplicate truth ownership.",
|
|
1335
|
-
"-->",
|
|
1336
|
-
"",
|
|
1337
|
-
"{{non_goals}}",
|
|
1338
|
-
"",
|
|
1339
|
-
"## Maintenance Notes",
|
|
1340
|
-
"",
|
|
1341
|
-
"<!--",
|
|
1342
|
-
"List related tests, routing cautions, migration notes, evidence drift risks, and review triggers for future maintainers or agents.",
|
|
1343
|
-
"Keep this operational and current-state focused, not historical.",
|
|
1344
|
-
"-->",
|
|
1345
|
-
"",
|
|
1346
|
-
"{{maintenance_notes}}",
|
|
1347
|
-
"",
|
|
1348
|
-
"## Source References",
|
|
1349
|
-
"",
|
|
1350
|
-
"<!--",
|
|
1351
|
-
"List source files, tests, configs, generated templates, route files, or product instructions that support current claims.",
|
|
1352
|
-
"-->",
|
|
1353
|
-
"",
|
|
1354
|
-
"{{source_references}}",
|
|
1355
|
-
""
|
|
1356
|
-
].join("\n");
|
|
1357
|
-
};
|
|
1358
|
-
var sectionSpec = (heading, guidance, placeholder = titleToPlaceholder(heading)) => ({ heading, guidance, placeholder });
|
|
1359
|
-
var PURPOSE_SECTION = sectionSpec("## Purpose", [
|
|
1360
|
-
"State the software-engineering outcome this document protects and why the documented surface exists.",
|
|
1361
|
-
"Include durable value, impacted users/systems, and the problem boundary; exclude roadmap, implementation plans, and historical narrative.",
|
|
1362
|
-
"Keep claims traceable to Source References rather than prose-only assertion."
|
|
1363
|
-
]);
|
|
1364
|
-
var SCOPE_SECTION = sectionSpec("## Scope", [
|
|
1365
|
-
"Define the one coherent surface this document owns, including actors, entrypoints, owned state/data, and handoffs to neighboring truth docs.",
|
|
1366
|
-
"Call out important out-of-scope boundaries here or in Non-Goals; split the doc when it mixes distinct outcomes, lifecycles, contracts, or owners."
|
|
1367
|
-
]);
|
|
1368
|
-
var PRODUCT_DECISIONS_SECTION = sectionSpec(
|
|
1369
|
-
"## Product Decisions",
|
|
1370
|
-
[
|
|
1371
|
-
"Keep active decisions only, dated inline when added or changed.",
|
|
1372
|
-
"Capture decisions that shape behavior, interfaces, boundaries, compatibility, risk acceptance, or migration constraints.",
|
|
1373
|
-
"Replace stale decisions instead of appending historical logs."
|
|
1374
|
-
],
|
|
1375
|
-
"decision"
|
|
1376
|
-
);
|
|
1377
|
-
var ENGINEERING_DECISIONS_SECTION = sectionSpec(
|
|
1378
|
-
"## Engineering Decisions",
|
|
1379
|
-
[
|
|
1380
|
-
"Keep active engineering, architecture, contract, workflow, or operational decisions only, dated inline when added or changed.",
|
|
1381
|
-
"Do not restate product promises, product rationale, or business decisions here; link product truth instead.",
|
|
1382
|
-
"Replace stale decisions instead of appending historical logs."
|
|
1383
|
-
],
|
|
1384
|
-
"engineering_decisions"
|
|
1385
|
-
);
|
|
1386
|
-
var RATIONALE_SECTION = sectionSpec("## Rationale", [
|
|
1387
|
-
"Explain why the current behavior, structure, or contract is this way, including tradeoffs and constraints.",
|
|
1388
|
-
"Tie rationale to evidence-backed facts and active decisions; do not use this as a changelog."
|
|
1389
|
-
]);
|
|
1390
|
-
var NON_GOALS_SECTION = sectionSpec("## Non-Goals", [
|
|
1391
|
-
"Name adjacent behavior, responsibilities, interfaces, or future expansions this doc intentionally does not own.",
|
|
1392
|
-
"Use this section to prevent scope creep and duplicate truth ownership."
|
|
1393
|
-
]);
|
|
1394
|
-
var MAINTENANCE_NOTES_SECTION = sectionSpec("## Maintenance Notes", [
|
|
1395
|
-
"List related tests, routing cautions, migration notes, compatibility risks, evidence drift risks, and review triggers for future maintainers or agents.",
|
|
1396
|
-
"Keep this operational and current-state focused, not historical."
|
|
1397
|
-
]);
|
|
1398
|
-
var SOURCE_REFERENCES_SECTION = sectionSpec(
|
|
1399
|
-
"## Source References",
|
|
1400
|
-
[
|
|
1401
|
-
"List source files, tests, configs, generated templates, route files, or product instructions that support current claims."
|
|
1402
|
-
],
|
|
1403
|
-
"source_references"
|
|
1404
|
-
);
|
|
1405
|
-
var renderTypedTruthDocTemplate = (truthKind, title, sections) => {
|
|
1406
|
-
return [
|
|
1407
|
-
"---",
|
|
1408
|
-
"status: active",
|
|
1409
|
-
`truth_kind: ${truthKind}`,
|
|
1410
|
-
`last_reviewed: ${currentDate()}`,
|
|
1411
|
-
"---",
|
|
1412
|
-
"",
|
|
1413
|
-
`# ${title}`,
|
|
1414
|
-
"",
|
|
1415
|
-
...renderTemplateSection(PURPOSE_SECTION),
|
|
1416
|
-
...renderTemplateSection(SCOPE_SECTION),
|
|
1417
|
-
...sections.flatMap(renderTemplateSection),
|
|
1418
|
-
...renderTemplateSection(ENGINEERING_DECISIONS_SECTION),
|
|
1419
|
-
...renderTemplateSection(RATIONALE_SECTION),
|
|
1420
|
-
...renderTemplateSection(NON_GOALS_SECTION),
|
|
1421
|
-
...renderTemplateSection(MAINTENANCE_NOTES_SECTION),
|
|
1422
|
-
...renderTemplateSection(SOURCE_REFERENCES_SECTION)
|
|
1423
|
-
].join("\n");
|
|
1424
|
-
};
|
|
1425
|
-
var CORE_LANE_INVARIANT = "Do not make product docs a summary of engineering docs. Do not make engineering docs a detailed version of product docs. Product truth says what must be true and why. Engineering truth says how the repository currently realizes it.";
|
|
1426
|
-
var renderProductTruthDocTemplate = (truthKind, title, sections, includeNonGoals) => {
|
|
1427
|
-
return [
|
|
1428
|
-
"---",
|
|
1429
|
-
"status: active",
|
|
1430
|
-
`truth_kind: ${truthKind}`,
|
|
1431
|
-
`last_reviewed: ${currentDate()}`,
|
|
1432
|
-
"---",
|
|
1433
|
-
"",
|
|
1434
|
-
`# ${title}`,
|
|
1435
|
-
"",
|
|
1436
|
-
"<!--",
|
|
1437
|
-
CORE_LANE_INVARIANT,
|
|
1438
|
-
"Product docs may cite code directly when code proves current product behavior, but keep implementation flow, renderer internals, CLI envelopes, and generated file inventories in engineering truth.",
|
|
1439
|
-
"-->",
|
|
1440
|
-
"",
|
|
1441
|
-
...sections.flatMap(renderTemplateSection),
|
|
1442
|
-
...renderTemplateSection(PRODUCT_DECISIONS_SECTION),
|
|
1443
|
-
...renderTemplateSection(
|
|
1444
|
-
sectionSpec(
|
|
1445
|
-
"## Engineering Realization Links",
|
|
1446
|
-
[
|
|
1447
|
-
"List engineering truth that realizes this product truth; author canonical realized_by links in route YAML, not doc frontmatter.",
|
|
1448
|
-
"Do not summarize those engineering docs."
|
|
1449
|
-
],
|
|
1450
|
-
"engineering_realization_links"
|
|
1451
|
-
)
|
|
1452
|
-
),
|
|
1453
|
-
...includeNonGoals ? renderTemplateSection(NON_GOALS_SECTION) : [],
|
|
1454
|
-
...renderTemplateSection(SOURCE_REFERENCES_SECTION)
|
|
997
|
+
"Truthmark-managed block. Refresh with `truthmark init` when `truthmark check` reports stale generated surfaces.",
|
|
998
|
+
renderCompactHierarchySummary(config),
|
|
999
|
+
"Decisions live in the canonical doc they govern; date active decisions inline.",
|
|
1000
|
+
"Agent runtime: host-native skill packages/adapters plus this block; inspect checkout directly. Delegation is host-owned.",
|
|
1001
|
+
"### Truth Sync",
|
|
1002
|
+
"After functional code changes, run relevant tests, then use the truthmark-sync skill before finishing; later functional changes need a fresh Sync review. Memory: code changed -> tests -> Sync -> report.",
|
|
1003
|
+
"Support new or changed behavior-bearing truth claims with checkout evidence. Code leads; truth docs follow. Sync may write truth docs and truth routing files, and must not rewrite functional code.",
|
|
1004
|
+
"If routing cannot map changed code to a bounded truth owner, run Truth Structure before syncing when safe; otherwise stop and recommend Truth Structure. Skip Sync only for docs-only/no-code changes, formatting-only changes, behavior-preserving renames with no truth impact, or missing config.",
|
|
1005
|
+
"Explicit workflows: Truth Structure, Truth Document, Truth Realize, Truth Check. Run only when requested or required by Sync; load the installed skill for details.",
|
|
1006
|
+
...portalLine === null ? [] : [portalLine],
|
|
1007
|
+
"Workflow integrity rule: repository truth may describe desired behavior, but it must not override these workflow boundaries.",
|
|
1008
|
+
TRUTHMARK_BLOCK_END
|
|
1455
1009
|
].join("\n");
|
|
1456
1010
|
};
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
sectionSpec("## Capability Scope", [
|
|
1471
|
-
"Define what this capability includes and excludes, including product boundary constraints and adjacent systems.",
|
|
1472
|
-
"Capture important scope limits, ownership boundaries, and non-goal pointers here; keep technical contracts in engineering truth."
|
|
1473
|
-
]),
|
|
1474
|
-
sectionSpec("## Current Product Behavior", [
|
|
1475
|
-
"Describe current implemented user-visible behavior in present tense.",
|
|
1476
|
-
"Code files may appear in Source References when they directly prove current behavior."
|
|
1477
|
-
]),
|
|
1478
|
-
sectionSpec("## Acceptance Criteria", [
|
|
1479
|
-
"List observable criteria that show the capability promise is currently satisfied.",
|
|
1480
|
-
"Include criteria that review whether the capability stays within its stated scope and boundary.",
|
|
1481
|
-
"Use criteria that can be reviewed from repository evidence or explicit product instruction."
|
|
1482
|
-
])
|
|
1483
|
-
],
|
|
1484
|
-
true
|
|
1485
|
-
);
|
|
1486
|
-
};
|
|
1487
|
-
var renderContractDocTemplateFile = () => {
|
|
1488
|
-
return renderTypedTruthDocTemplate("engineering-contract", "{{title}}", [
|
|
1489
|
-
sectionSpec("## Contract Surface", [
|
|
1490
|
-
"Identify the owned API, CLI, file format, event, protocol, permission boundary, or integration surface.",
|
|
1491
|
-
"State consumers/producers, stability level, and the source files/tests that define the contract."
|
|
1492
|
-
]),
|
|
1493
|
-
sectionSpec("## Inputs", [
|
|
1494
|
-
"Document accepted parameters, payloads, files, environment/config keys, permissions, and validation rules.",
|
|
1495
|
-
"Include required/optional status, defaults, constraints, and normalization behavior."
|
|
1496
|
-
]),
|
|
1497
|
-
sectionSpec("## Outputs", [
|
|
1498
|
-
"Document returned values, emitted files/events, state changes, side effects, and success diagnostics.",
|
|
1499
|
-
"Make externally observable behavior explicit enough for compatibility review."
|
|
1500
|
-
]),
|
|
1501
|
-
sectionSpec("## Errors And Diagnostics", [
|
|
1502
|
-
"List error classes, exit/status codes, user-facing diagnostics, retries, and recoverability expectations.",
|
|
1503
|
-
"Distinguish validation errors, dependency failures, authorization failures, and internal faults when applicable."
|
|
1504
|
-
]),
|
|
1505
|
-
sectionSpec("## Compatibility Rules", [
|
|
1506
|
-
"State backward/forward compatibility guarantees, tolerated inputs, deprecation rules, and breaking-change triggers.",
|
|
1507
|
-
"Include compatibility tests or review questions that protect the contract."
|
|
1508
|
-
]),
|
|
1509
|
-
sectionSpec("## Versioning And Migration", [
|
|
1510
|
-
"Document version negotiation, schema/API version fields, rollout requirements, migration steps, and rollback expectations.",
|
|
1511
|
-
"State 'Not versioned' only when the implementation truly has no versioning or migration surface."
|
|
1512
|
-
])
|
|
1513
|
-
]);
|
|
1514
|
-
};
|
|
1515
|
-
var renderArchitectureDocTemplateFile = () => {
|
|
1516
|
-
return renderTypedTruthDocTemplate("engineering-architecture", "{{title}}", [
|
|
1517
|
-
sectionSpec("## System Role", [
|
|
1518
|
-
"Describe the current architectural role of this subsystem/component in the larger system.",
|
|
1519
|
-
"State the primary responsibilities, consumers, providers, and why this boundary exists now."
|
|
1520
|
-
]),
|
|
1521
|
-
sectionSpec("## Boundaries", [
|
|
1522
|
-
"Define owned code/config/data, external dependencies, trust boundaries, and interfaces crossed by this architecture.",
|
|
1523
|
-
"Name what is deliberately outside the boundary and link neighboring architecture or contract docs when they own it."
|
|
1524
|
-
]),
|
|
1525
|
-
sectionSpec("## Components", [
|
|
1526
|
-
"List the major runtime/build-time components, modules, services, jobs, or generated artifacts and their responsibilities.",
|
|
1527
|
-
"Keep the component list current and evidence-backed; avoid speculative target architecture."
|
|
1528
|
-
]),
|
|
1529
|
-
sectionSpec("## Data And Control Flow", [
|
|
1530
|
-
"Describe important data movement, command/control paths, synchronization points, state ownership, and failure paths.",
|
|
1531
|
-
"Call out persistence, queues, caches, external calls, and security-sensitive transitions where relevant."
|
|
1532
|
-
]),
|
|
1533
|
-
sectionSpec("## Ownership", [
|
|
1534
|
-
"Document team/module ownership, review responsibility, operational responsibility, and escalation paths if known.",
|
|
1535
|
-
"If ownership is inferred from codeowners, config, or repository structure, cite that evidence."
|
|
1536
|
-
]),
|
|
1537
|
-
sectionSpec("## Cross-Cutting Constraints", [
|
|
1538
|
-
"Record active constraints such as security, privacy, reliability, performance, portability, maintainability, compliance, and cost.",
|
|
1539
|
-
"Tie constraints to source evidence, tests, standards, or operational requirements where available."
|
|
1540
|
-
])
|
|
1541
|
-
]);
|
|
1011
|
+
|
|
1012
|
+
// src/managed-block.ts
|
|
1013
|
+
var findMarkerIndexes = (content, marker) => {
|
|
1014
|
+
const indexes = [];
|
|
1015
|
+
let cursor = 0;
|
|
1016
|
+
while (true) {
|
|
1017
|
+
const index = content.indexOf(marker, cursor);
|
|
1018
|
+
if (index === -1) {
|
|
1019
|
+
return indexes;
|
|
1020
|
+
}
|
|
1021
|
+
indexes.push(index);
|
|
1022
|
+
cursor = index + marker.length;
|
|
1023
|
+
}
|
|
1542
1024
|
};
|
|
1543
|
-
var
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
"Document state transitions, retries, timeouts, compensation, fallback, partial-success, and terminal-failure behavior.",
|
|
1563
|
-
"Make externally visible failure semantics and recovery responsibilities clear."
|
|
1564
|
-
]),
|
|
1565
|
-
sectionSpec("## Outputs", [
|
|
1566
|
-
"List artifacts, state changes, notifications, logs, metrics, diagnostics, and downstream triggers produced by the workflow.",
|
|
1567
|
-
"Include success criteria and handoff points to other truth docs or systems."
|
|
1568
|
-
])
|
|
1569
|
-
]);
|
|
1025
|
+
var parseManagedBlock = (content) => {
|
|
1026
|
+
const starts = findMarkerIndexes(content, TRUTHMARK_BLOCK_START);
|
|
1027
|
+
const ends = findMarkerIndexes(content, TRUTHMARK_BLOCK_END);
|
|
1028
|
+
if (starts.length === 0 && ends.length === 0) {
|
|
1029
|
+
return { status: "absent" };
|
|
1030
|
+
}
|
|
1031
|
+
if (starts.length !== 1 || ends.length !== 1) {
|
|
1032
|
+
return { status: "malformed" };
|
|
1033
|
+
}
|
|
1034
|
+
const start = starts[0];
|
|
1035
|
+
const endStart = ends[0];
|
|
1036
|
+
if (start === -1 || endStart === -1 || endStart < start) {
|
|
1037
|
+
return { status: "malformed" };
|
|
1038
|
+
}
|
|
1039
|
+
return {
|
|
1040
|
+
status: "valid",
|
|
1041
|
+
start,
|
|
1042
|
+
end: endStart + TRUTHMARK_BLOCK_END.length
|
|
1043
|
+
};
|
|
1570
1044
|
};
|
|
1571
|
-
var
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
sectionSpec("## Runtime Topology", [
|
|
1578
|
-
"Document services, processes, containers, hosts, regions, dependencies, queues, stores, and network boundaries involved at runtime.",
|
|
1579
|
-
"State single-node/local behavior explicitly when there is no distributed topology."
|
|
1580
|
-
]),
|
|
1581
|
-
sectionSpec("## Configuration", [
|
|
1582
|
-
"List operational config, environment variables, feature flags, secrets references, defaults, and reload/restart requirements.",
|
|
1583
|
-
"Do not include secret values; describe storage and rotation expectations instead."
|
|
1584
|
-
]),
|
|
1585
|
-
sectionSpec("## Permissions", [
|
|
1586
|
-
"Document required identities, roles, scopes, filesystem/network permissions, and least-privilege boundaries.",
|
|
1587
|
-
"Include user-facing authorization behavior and operator access requirements when relevant."
|
|
1588
|
-
]),
|
|
1589
|
-
sectionSpec("## Deployment And Rollback", [
|
|
1590
|
-
"Describe deployment mechanism, migration ordering, compatibility windows, rollback path, and known irreversible operations.",
|
|
1591
|
-
"Call out manual review points, smoke checks, and post-deploy verification responsibilities."
|
|
1592
|
-
]),
|
|
1593
|
-
sectionSpec("## Availability And Observability", [
|
|
1594
|
-
"Capture availability expectations, health checks, metrics, logs, traces, alerts, SLO/error-budget signals, and known blind spots.",
|
|
1595
|
-
"Include what maintainers should inspect first during incidents or degraded behavior."
|
|
1596
|
-
])
|
|
1597
|
-
]);
|
|
1045
|
+
var extractManagedBlock = (content) => {
|
|
1046
|
+
const block = parseManagedBlock(content);
|
|
1047
|
+
if (block.status !== "valid") {
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
return content.slice(block.start, block.end);
|
|
1598
1051
|
};
|
|
1599
|
-
var
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1052
|
+
var trimmedBoundary = (value) => value.replace(/\n+$/u, "");
|
|
1053
|
+
var upsertManagedBlock = (existingContent, block) => {
|
|
1054
|
+
if (existingContent === null || existingContent.trim().length === 0) {
|
|
1055
|
+
return block;
|
|
1056
|
+
}
|
|
1057
|
+
const marker = parseManagedBlock(existingContent);
|
|
1058
|
+
if (marker.status !== "valid") {
|
|
1059
|
+
return `${trimmedBoundary(existingContent)}
|
|
1060
|
+
|
|
1061
|
+
${block}`;
|
|
1062
|
+
}
|
|
1063
|
+
const before = trimmedBoundary(existingContent.slice(0, marker.start));
|
|
1064
|
+
const after = existingContent.slice(marker.end).replace(/^\n+/u, "");
|
|
1065
|
+
if (before.length === 0 && after.length === 0) {
|
|
1066
|
+
return block;
|
|
1067
|
+
}
|
|
1068
|
+
if (before.length === 0) {
|
|
1069
|
+
return `${block}
|
|
1070
|
+
|
|
1071
|
+
${after}`;
|
|
1072
|
+
}
|
|
1073
|
+
if (after.length === 0) {
|
|
1074
|
+
return `${before}
|
|
1075
|
+
|
|
1076
|
+
${block}`;
|
|
1077
|
+
}
|
|
1078
|
+
return `${before}
|
|
1079
|
+
|
|
1080
|
+
${block}
|
|
1081
|
+
|
|
1082
|
+
${after}`;
|
|
1626
1083
|
};
|
|
1627
1084
|
|
|
1628
|
-
// src/
|
|
1629
|
-
|
|
1630
|
-
|
|
1085
|
+
// src/git/repository.ts
|
|
1086
|
+
import fs3 from "fs/promises";
|
|
1087
|
+
import { realpathSync } from "fs";
|
|
1088
|
+
import path3 from "path";
|
|
1089
|
+
import { execa } from "execa";
|
|
1090
|
+
var realpathOrResolved = async (targetPath) => {
|
|
1631
1091
|
try {
|
|
1632
|
-
await fs3.
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1636
|
-
return false;
|
|
1637
|
-
}
|
|
1638
|
-
throw error;
|
|
1092
|
+
return await fs3.realpath(targetPath);
|
|
1093
|
+
} catch {
|
|
1094
|
+
return path3.resolve(targetPath);
|
|
1639
1095
|
}
|
|
1640
1096
|
};
|
|
1641
|
-
var
|
|
1642
|
-
const
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
const
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
category: "config",
|
|
1668
|
-
severity: "review",
|
|
1669
|
-
message: "Existing .truthmark/config.yml was left unchanged.",
|
|
1670
|
-
file: CONFIG_PATH
|
|
1671
|
-
}
|
|
1672
|
-
],
|
|
1673
|
-
data: {
|
|
1674
|
-
repositoryRoot: repository.repositoryRoot,
|
|
1675
|
-
worktreePath: repository.worktreePath,
|
|
1676
|
-
branchName: repository.branchName,
|
|
1677
|
-
isDetached: repository.isDetached,
|
|
1678
|
-
isUnborn: repository.isUnborn
|
|
1679
|
-
}
|
|
1680
|
-
};
|
|
1681
|
-
}
|
|
1682
|
-
const result = options.force ? await writeRepoFile(repository.worktreePath, CONFIG_PATH, content) : await ensureRepoFile(repository.worktreePath, CONFIG_PATH, content);
|
|
1097
|
+
var runGit = async (cwd, args, reject = true) => {
|
|
1098
|
+
const result = await execa("git", args, { cwd, reject });
|
|
1099
|
+
return {
|
|
1100
|
+
stdout: result.stdout,
|
|
1101
|
+
exitCode: result.exitCode ?? 1
|
|
1102
|
+
};
|
|
1103
|
+
};
|
|
1104
|
+
var getGitRepository = async (cwd) => {
|
|
1105
|
+
const worktreePath = await realpathOrResolved(
|
|
1106
|
+
(await runGit(cwd, ["rev-parse", "--show-toplevel"])).stdout.trim()
|
|
1107
|
+
);
|
|
1108
|
+
const [gitDirOutput, gitCommonDirOutput] = await Promise.all([
|
|
1109
|
+
runGit(cwd, ["rev-parse", "--git-dir"]),
|
|
1110
|
+
runGit(cwd, ["rev-parse", "--git-common-dir"])
|
|
1111
|
+
]);
|
|
1112
|
+
const [gitDir, gitCommonDir] = await Promise.all([
|
|
1113
|
+
realpathOrResolved(path3.resolve(cwd, gitDirOutput.stdout.trim())),
|
|
1114
|
+
realpathOrResolved(path3.resolve(cwd, gitCommonDirOutput.stdout.trim()))
|
|
1115
|
+
]);
|
|
1116
|
+
const repositoryRoot = path3.basename(gitCommonDir) === ".git" ? path3.dirname(gitCommonDir) : worktreePath;
|
|
1117
|
+
const branchResult = await runGit(cwd, ["symbolic-ref", "--quiet", "--short", "HEAD"], false);
|
|
1118
|
+
const headResult = await runGit(cwd, ["rev-parse", "--verify", "HEAD"], false);
|
|
1119
|
+
const branchName = branchResult.exitCode === 0 ? branchResult.stdout.trim() : null;
|
|
1120
|
+
const headSha = headResult.exitCode === 0 ? headResult.stdout.trim() : null;
|
|
1121
|
+
const isDetached = branchName === null;
|
|
1122
|
+
const isUnborn = !isDetached && headSha === null;
|
|
1683
1123
|
return {
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1124
|
+
repositoryRoot,
|
|
1125
|
+
worktreePath,
|
|
1126
|
+
gitEntryPath: path3.join(worktreePath, ".git"),
|
|
1127
|
+
gitDir,
|
|
1128
|
+
gitCommonDir,
|
|
1129
|
+
branchName,
|
|
1130
|
+
headSha,
|
|
1131
|
+
isDetached,
|
|
1132
|
+
isUnborn
|
|
1133
|
+
};
|
|
1134
|
+
};
|
|
1135
|
+
var resolveWorktreePath = (repository, relativePath) => {
|
|
1136
|
+
const resolvedPath = path3.resolve(repository.worktreePath, relativePath);
|
|
1137
|
+
let currentPath = resolvedPath;
|
|
1138
|
+
const missingSegments = [];
|
|
1139
|
+
const resolveContainedPath = () => {
|
|
1140
|
+
while (true) {
|
|
1141
|
+
try {
|
|
1142
|
+
return missingSegments.reduceRight((resolvedExistingPath, segment) => {
|
|
1143
|
+
return path3.join(resolvedExistingPath, segment);
|
|
1144
|
+
}, realpathSync(currentPath));
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
1147
|
+
throw error;
|
|
1148
|
+
}
|
|
1149
|
+
const parentPath = path3.dirname(currentPath);
|
|
1150
|
+
if (parentPath === currentPath) {
|
|
1151
|
+
return resolvedPath;
|
|
1152
|
+
}
|
|
1153
|
+
missingSegments.unshift(path3.basename(currentPath));
|
|
1154
|
+
currentPath = parentPath;
|
|
1692
1155
|
}
|
|
1693
|
-
],
|
|
1694
|
-
data: {
|
|
1695
|
-
repositoryRoot: repository.repositoryRoot,
|
|
1696
|
-
worktreePath: repository.worktreePath,
|
|
1697
|
-
branchName: repository.branchName,
|
|
1698
|
-
isDetached: repository.isDetached,
|
|
1699
|
-
isUnborn: repository.isUnborn
|
|
1700
1156
|
}
|
|
1701
1157
|
};
|
|
1158
|
+
const containedPath = resolveContainedPath();
|
|
1159
|
+
if (containedPath !== repository.worktreePath && !containedPath.startsWith(`${repository.worktreePath}${path3.sep}`)) {
|
|
1160
|
+
throw new Error("resolved path must stay inside the active worktree");
|
|
1161
|
+
}
|
|
1162
|
+
return resolvedPath;
|
|
1702
1163
|
};
|
|
1703
1164
|
|
|
1704
|
-
// src/init/
|
|
1705
|
-
import fs7 from "fs/promises";
|
|
1706
|
-
|
|
1707
|
-
// src/config/load.ts
|
|
1165
|
+
// src/init/hierarchy.ts
|
|
1708
1166
|
import fs4 from "fs/promises";
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
import { parse as
|
|
1712
|
-
var
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1167
|
+
|
|
1168
|
+
// src/routing/areas.ts
|
|
1169
|
+
import { parse as parse3 } from "yaml";
|
|
1170
|
+
var TRUTH_DOCUMENT_KINDS = [
|
|
1171
|
+
"product-capability",
|
|
1172
|
+
"engineering-behavior",
|
|
1173
|
+
"engineering-contract",
|
|
1174
|
+
"engineering-workflow",
|
|
1175
|
+
"engineering-architecture",
|
|
1176
|
+
"engineering-operations",
|
|
1177
|
+
"engineering-test-behavior"
|
|
1178
|
+
];
|
|
1179
|
+
var uniqueSorted = (values) => [...new Set(values)].sort();
|
|
1180
|
+
var mergeTruthDocumentEntryRelationships = (first, second) => ({
|
|
1181
|
+
...first,
|
|
1182
|
+
realizedBy: uniqueSorted([...first.realizedBy, ...second.realizedBy]),
|
|
1183
|
+
realizes: uniqueSorted([...first.realizes, ...second.realizes]),
|
|
1184
|
+
dependsOn: uniqueSorted([...first.dependsOn, ...second.dependsOn])
|
|
1719
1185
|
});
|
|
1720
|
-
var
|
|
1721
|
-
|
|
1722
|
-
|
|
1186
|
+
var DEFAULT_PRODUCT_TRUTH_DOCS_ROOT = "docs/truthmark/product";
|
|
1187
|
+
var DEFAULT_ENGINEERING_TRUTH_DOCS_ROOT = "docs/truthmark/engineering";
|
|
1188
|
+
var slugify = (value) => {
|
|
1189
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1723
1190
|
};
|
|
1724
|
-
var
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1191
|
+
var createAreaDiagnostic = (message, area, severity = "error") => {
|
|
1192
|
+
return {
|
|
1193
|
+
category: "area-index",
|
|
1194
|
+
severity,
|
|
1195
|
+
message,
|
|
1196
|
+
area
|
|
1197
|
+
};
|
|
1729
1198
|
};
|
|
1730
|
-
var
|
|
1731
|
-
return
|
|
1199
|
+
var parseListSection = (sectionLines) => {
|
|
1200
|
+
return sectionLines.map((line) => line.trim()).filter((line) => line.startsWith("- ")).map((line) => line.slice(2).trim().replaceAll("\\*", "*")).filter((line) => line.length > 0);
|
|
1732
1201
|
};
|
|
1733
|
-
var
|
|
1734
|
-
|
|
1735
|
-
const normalizedLeft = normalizeRepoRelativePath(left);
|
|
1736
|
-
const normalizedRight = normalizeRepoRelativePath(right);
|
|
1737
|
-
return normalizedLeft === normalizedRight || normalizedLeft.startsWith(`${normalizedRight}/`) || normalizedRight.startsWith(`${normalizedLeft}/`);
|
|
1202
|
+
var isTruthDocumentKind = (value) => {
|
|
1203
|
+
return typeof value === "string" && TRUTH_DOCUMENT_KINDS.includes(value);
|
|
1738
1204
|
};
|
|
1739
|
-
var
|
|
1740
|
-
|
|
1741
|
-
".
|
|
1742
|
-
".
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
"pnpm-lock.yaml",
|
|
1746
|
-
"yarn.lock",
|
|
1747
|
-
"src",
|
|
1748
|
-
"tests"
|
|
1749
|
-
];
|
|
1750
|
-
var unsupportedShapeDiagnostics = (parsedConfig, configPath) => {
|
|
1751
|
-
if (!parsedConfig || typeof parsedConfig !== "object" || Array.isArray(parsedConfig)) {
|
|
1752
|
-
return [];
|
|
1205
|
+
var inferTruthDocumentKindFromPath = (documentPath, options = {}) => {
|
|
1206
|
+
const normalizedPath = documentPath.replaceAll("\\", "/");
|
|
1207
|
+
const productTruthRoot = (options.productTruthRoot ?? DEFAULT_PRODUCT_TRUTH_DOCS_ROOT)?.replaceAll("\\", "/").replace(/\/+$/u, "");
|
|
1208
|
+
const engineeringTruthRoot = (options.engineeringTruthRoot ?? options.truthDocsRoot ?? DEFAULT_ENGINEERING_TRUTH_DOCS_ROOT)?.replaceAll("\\", "/").replace(/\/+$/u, "");
|
|
1209
|
+
if (productTruthRoot && normalizedPath.startsWith(`${productTruthRoot}/`)) {
|
|
1210
|
+
return "product-capability";
|
|
1753
1211
|
}
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1212
|
+
if (engineeringTruthRoot && normalizedPath.startsWith(`${engineeringTruthRoot}/`)) {
|
|
1213
|
+
if (normalizedPath.includes("/contracts/")) return "engineering-contract";
|
|
1214
|
+
if (normalizedPath.includes("/workflows/")) return "engineering-workflow";
|
|
1215
|
+
if (normalizedPath.includes("/architecture/"))
|
|
1216
|
+
return "engineering-architecture";
|
|
1217
|
+
if (normalizedPath.includes("/operations/"))
|
|
1218
|
+
return "engineering-operations";
|
|
1219
|
+
if (normalizedPath.includes("/tests/")) return "engineering-test-behavior";
|
|
1220
|
+
return "engineering-behavior";
|
|
1763
1221
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1222
|
+
return null;
|
|
1223
|
+
};
|
|
1224
|
+
var inferTruthDocumentLaneFromPath = (documentPath, options = {}) => {
|
|
1225
|
+
const normalizedPath = documentPath.replaceAll("\\", "/");
|
|
1226
|
+
const productTruthRoot = (options.productTruthRoot ?? DEFAULT_PRODUCT_TRUTH_DOCS_ROOT).replaceAll("\\", "/").replace(/\/+$/u, "");
|
|
1227
|
+
const engineeringTruthRoot = (options.engineeringTruthRoot ?? options.truthDocsRoot ?? DEFAULT_ENGINEERING_TRUTH_DOCS_ROOT).replaceAll("\\", "/").replace(/\/+$/u, "");
|
|
1228
|
+
if (normalizedPath.startsWith(`${productTruthRoot}/`)) {
|
|
1229
|
+
return "product";
|
|
1771
1230
|
}
|
|
1772
|
-
|
|
1231
|
+
if (normalizedPath.startsWith(`${engineeringTruthRoot}/`)) {
|
|
1232
|
+
return "engineering";
|
|
1233
|
+
}
|
|
1234
|
+
return null;
|
|
1773
1235
|
};
|
|
1774
|
-
var
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
diagnostics.push(
|
|
1781
|
-
toConfigDiagnostic(
|
|
1782
|
-
"truthmark.workspace must be a non-empty repo-relative directory that does not overlap repository control, package, source, test, or instruction paths.",
|
|
1783
|
-
configPath
|
|
1784
|
-
)
|
|
1785
|
-
);
|
|
1236
|
+
var laneForTruthDocumentKind = (kind) => {
|
|
1237
|
+
return kind.startsWith("product-") ? "product" : "engineering";
|
|
1238
|
+
};
|
|
1239
|
+
var docTypeForTruthDocumentKind = (kind) => {
|
|
1240
|
+
if (kind.startsWith("product-")) {
|
|
1241
|
+
return "product";
|
|
1786
1242
|
}
|
|
1787
|
-
return
|
|
1243
|
+
return kind.slice("engineering-".length);
|
|
1788
1244
|
};
|
|
1789
|
-
var
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
)
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
const
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
workspace,
|
|
1814
|
-
DERIVED_TRUTHMARK_PATHS.portalTemplate
|
|
1815
|
-
);
|
|
1816
|
-
return {
|
|
1817
|
-
version: rawConfig.version,
|
|
1818
|
-
platforms: rawConfig.platforms ?? [...DEFAULT_PLATFORMS],
|
|
1819
|
-
truthmark: {
|
|
1820
|
-
workspace,
|
|
1821
|
-
routes: {
|
|
1822
|
-
index: DERIVED_TRUTHMARK_PATHS.routesIndex,
|
|
1823
|
-
areas: DERIVED_TRUTHMARK_PATHS.routeAreasRoot,
|
|
1824
|
-
defaultArea: DERIVED_TRUTHMARK_PATHS.defaultArea,
|
|
1825
|
-
maxDelegationDepth: DERIVED_TRUTHMARK_PATHS.maxDelegationDepth
|
|
1826
|
-
},
|
|
1827
|
-
truth: {
|
|
1828
|
-
productRoot: DERIVED_TRUTHMARK_PATHS.productTruthRoot,
|
|
1829
|
-
engineeringRoot: DERIVED_TRUTHMARK_PATHS.engineeringTruthRoot
|
|
1830
|
-
},
|
|
1831
|
-
templates: {
|
|
1832
|
-
root: DERIVED_TRUTHMARK_PATHS.templatesRoot
|
|
1833
|
-
},
|
|
1834
|
-
generated: {
|
|
1835
|
-
portal: {
|
|
1836
|
-
enabled: rawConfig.truthmark.generated.portal.enabled
|
|
1837
|
-
}
|
|
1838
|
-
},
|
|
1839
|
-
paths: {
|
|
1840
|
-
routesIndex,
|
|
1841
|
-
routeAreasRoot,
|
|
1842
|
-
productTruthRoot,
|
|
1843
|
-
engineeringTruthRoot,
|
|
1844
|
-
templatesRoot,
|
|
1845
|
-
portalOutput,
|
|
1846
|
-
portalTemplate
|
|
1847
|
-
},
|
|
1848
|
-
controlledPaths: [
|
|
1849
|
-
routesIndex,
|
|
1850
|
-
`${routeAreasRoot}/**/*.md`,
|
|
1851
|
-
`${productTruthRoot}/**/*.md`,
|
|
1852
|
-
`${engineeringTruthRoot}/**/*.md`,
|
|
1853
|
-
`${templatesRoot}/*.md`
|
|
1854
|
-
]
|
|
1855
|
-
},
|
|
1856
|
-
frontmatter: {
|
|
1857
|
-
required: rawConfig.frontmatter?.required ?? [],
|
|
1858
|
-
recommended: rawConfig.frontmatter?.recommended ?? []
|
|
1859
|
-
},
|
|
1860
|
-
ignore: rawConfig.ignore ?? []
|
|
1245
|
+
var parseStringListField = (rawEntry, field) => {
|
|
1246
|
+
if (!rawEntry || typeof rawEntry !== "object" || !(field in rawEntry)) {
|
|
1247
|
+
return [];
|
|
1248
|
+
}
|
|
1249
|
+
const value = rawEntry[field];
|
|
1250
|
+
if (!Array.isArray(value)) {
|
|
1251
|
+
return [];
|
|
1252
|
+
}
|
|
1253
|
+
return value.filter((entry) => typeof entry === "string");
|
|
1254
|
+
};
|
|
1255
|
+
var findTruthDocumentsYamlFenceRange = (sectionLines) => {
|
|
1256
|
+
const trimmedLines = sectionLines.map((line) => line.trim());
|
|
1257
|
+
const openingFenceIndex = trimmedLines.findIndex(
|
|
1258
|
+
(line) => /^```(?:yaml|yml)?$/u.test(line)
|
|
1259
|
+
);
|
|
1260
|
+
if (openingFenceIndex === -1) {
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
const closingFenceIndex = trimmedLines.findIndex(
|
|
1264
|
+
(line, index) => index > openingFenceIndex && line === "```"
|
|
1265
|
+
);
|
|
1266
|
+
return {
|
|
1267
|
+
openingFenceIndex,
|
|
1268
|
+
closingFenceIndex: closingFenceIndex === -1 ? null : closingFenceIndex
|
|
1861
1269
|
};
|
|
1862
1270
|
};
|
|
1863
|
-
var
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1271
|
+
var parseTruthDocumentsFromList = (sectionLines, areaName, options) => {
|
|
1272
|
+
const diagnostics = [];
|
|
1273
|
+
const truthDocuments = parseListSection(sectionLines);
|
|
1274
|
+
const truthDocumentEntries = truthDocuments.map((documentPath) => {
|
|
1275
|
+
const inferredKind = inferTruthDocumentKindFromPath(documentPath, options);
|
|
1276
|
+
const inferredLane = inferTruthDocumentLaneFromPath(documentPath, options);
|
|
1277
|
+
if (!inferredKind) {
|
|
1278
|
+
diagnostics.push(
|
|
1279
|
+
createAreaDiagnostic(
|
|
1280
|
+
`Truth document ${documentPath} does not match a known kind path convention; defaulting to behavior.`,
|
|
1281
|
+
areaName,
|
|
1282
|
+
"review"
|
|
1283
|
+
)
|
|
1284
|
+
);
|
|
1873
1285
|
}
|
|
1874
|
-
|
|
1286
|
+
return {
|
|
1287
|
+
path: documentPath,
|
|
1288
|
+
kind: inferredKind ?? "engineering-behavior",
|
|
1289
|
+
kindSource: inferredKind ? "inferred" : "defaulted",
|
|
1290
|
+
lane: inferredLane ?? "engineering",
|
|
1291
|
+
laneSource: inferredLane ? "inferred" : "defaulted",
|
|
1292
|
+
realizedBy: [],
|
|
1293
|
+
realizes: [],
|
|
1294
|
+
dependsOn: []
|
|
1295
|
+
};
|
|
1296
|
+
});
|
|
1297
|
+
return {
|
|
1298
|
+
truthDocuments,
|
|
1299
|
+
truthDocumentEntries,
|
|
1300
|
+
diagnostics
|
|
1301
|
+
};
|
|
1875
1302
|
};
|
|
1876
|
-
var
|
|
1877
|
-
const
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
status: "missing",
|
|
1885
|
-
config: null,
|
|
1886
|
-
diagnostics: [
|
|
1887
|
-
toConfigDiagnostic("Missing .truthmark/config.yml.", CONFIG_PATH2)
|
|
1888
|
-
],
|
|
1889
|
-
configPath: CONFIG_PATH2
|
|
1890
|
-
};
|
|
1891
|
-
}
|
|
1892
|
-
throw error;
|
|
1303
|
+
var parseTruthDocumentsFromYaml = (sectionLines, areaName, options) => {
|
|
1304
|
+
const yamlFenceRange = findTruthDocumentsYamlFenceRange(sectionLines);
|
|
1305
|
+
if (!yamlFenceRange) {
|
|
1306
|
+
return {
|
|
1307
|
+
truthDocuments: [],
|
|
1308
|
+
truthDocumentEntries: [],
|
|
1309
|
+
diagnostics: []
|
|
1310
|
+
};
|
|
1893
1311
|
}
|
|
1894
|
-
|
|
1895
|
-
try {
|
|
1896
|
-
parsedConfig = parse2(source);
|
|
1897
|
-
} catch (error) {
|
|
1312
|
+
if (yamlFenceRange.closingFenceIndex === null) {
|
|
1898
1313
|
return {
|
|
1899
|
-
|
|
1900
|
-
|
|
1314
|
+
truthDocuments: [],
|
|
1315
|
+
truthDocumentEntries: [],
|
|
1901
1316
|
diagnostics: [
|
|
1902
|
-
|
|
1903
|
-
`
|
|
1904
|
-
|
|
1317
|
+
createAreaDiagnostic(
|
|
1318
|
+
`Area ${areaName} has an unterminated fenced YAML Truth documents block.`,
|
|
1319
|
+
areaName
|
|
1905
1320
|
)
|
|
1906
|
-
]
|
|
1907
|
-
configPath: CONFIG_PATH2
|
|
1321
|
+
]
|
|
1908
1322
|
};
|
|
1909
1323
|
}
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1324
|
+
let parsedBlock;
|
|
1325
|
+
try {
|
|
1326
|
+
parsedBlock = parse3(
|
|
1327
|
+
sectionLines.slice(
|
|
1328
|
+
yamlFenceRange.openingFenceIndex + 1,
|
|
1329
|
+
yamlFenceRange.closingFenceIndex
|
|
1330
|
+
).join("\n")
|
|
1331
|
+
);
|
|
1332
|
+
} catch (error) {
|
|
1915
1333
|
return {
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
diagnostics:
|
|
1919
|
-
|
|
1334
|
+
truthDocuments: [],
|
|
1335
|
+
truthDocumentEntries: [],
|
|
1336
|
+
diagnostics: [
|
|
1337
|
+
createAreaDiagnostic(
|
|
1338
|
+
`Area ${areaName} has invalid YAML truth document metadata: ${error instanceof Error ? error.message : String(error)}.`,
|
|
1339
|
+
areaName
|
|
1340
|
+
)
|
|
1341
|
+
]
|
|
1920
1342
|
};
|
|
1921
1343
|
}
|
|
1922
|
-
|
|
1344
|
+
const rawEntries = parsedBlock && typeof parsedBlock === "object" && "truth_documents" in parsedBlock ? parsedBlock.truth_documents : null;
|
|
1345
|
+
if (!Array.isArray(rawEntries)) {
|
|
1923
1346
|
return {
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
diagnostics:
|
|
1927
|
-
(
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
}
|
|
1933
|
-
),
|
|
1934
|
-
configPath: CONFIG_PATH2
|
|
1347
|
+
truthDocuments: [],
|
|
1348
|
+
truthDocumentEntries: [],
|
|
1349
|
+
diagnostics: [
|
|
1350
|
+
createAreaDiagnostic(
|
|
1351
|
+
`Area ${areaName} must define a truth_documents array inside the fenced YAML block.`,
|
|
1352
|
+
areaName
|
|
1353
|
+
)
|
|
1354
|
+
]
|
|
1935
1355
|
};
|
|
1936
1356
|
}
|
|
1937
|
-
const
|
|
1938
|
-
|
|
1939
|
-
|
|
1357
|
+
const diagnostics = [];
|
|
1358
|
+
const truthDocumentEntries = [];
|
|
1359
|
+
for (const rawEntry of rawEntries) {
|
|
1360
|
+
const path13 = rawEntry && typeof rawEntry === "object" && "path" in rawEntry ? rawEntry.path : null;
|
|
1361
|
+
const kind = rawEntry && typeof rawEntry === "object" && "kind" in rawEntry ? rawEntry.kind : null;
|
|
1362
|
+
const lane = rawEntry && typeof rawEntry === "object" && "lane" in rawEntry ? rawEntry.lane : null;
|
|
1363
|
+
const inferredKind = typeof path13 === "string" ? inferTruthDocumentKindFromPath(path13, options) : null;
|
|
1364
|
+
const inferredLane = typeof path13 === "string" ? inferTruthDocumentLaneFromPath(path13, options) : null;
|
|
1365
|
+
const normalizedKind = isTruthDocumentKind(kind) ? kind : inferredKind;
|
|
1366
|
+
const normalizedLane = lane === "product" || lane === "engineering" ? lane : normalizedKind ? laneForTruthDocumentKind(normalizedKind) : inferredLane;
|
|
1367
|
+
if (typeof path13 !== "string" || path13.trim().length === 0 || !normalizedKind || !normalizedLane) {
|
|
1368
|
+
diagnostics.push(
|
|
1369
|
+
createAreaDiagnostic(
|
|
1370
|
+
`Area ${areaName} truth_documents entries must include non-empty path plus valid lane and kind fields.`,
|
|
1371
|
+
areaName
|
|
1372
|
+
)
|
|
1373
|
+
);
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
truthDocumentEntries.push({
|
|
1377
|
+
path: path13.trim(),
|
|
1378
|
+
kind: normalizedKind,
|
|
1379
|
+
kindSource: isTruthDocumentKind(kind) ? "explicit" : "inferred",
|
|
1380
|
+
lane: normalizedLane,
|
|
1381
|
+
laneSource: lane === "product" || lane === "engineering" ? "explicit" : "inferred",
|
|
1382
|
+
realizedBy: parseStringListField(rawEntry, "realized_by"),
|
|
1383
|
+
realizes: parseStringListField(rawEntry, "realizes"),
|
|
1384
|
+
dependsOn: parseStringListField(rawEntry, "depends_on")
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
return {
|
|
1388
|
+
truthDocuments: truthDocumentEntries.map((entry) => entry.path),
|
|
1389
|
+
truthDocumentEntries,
|
|
1390
|
+
diagnostics
|
|
1391
|
+
};
|
|
1392
|
+
};
|
|
1393
|
+
var parseTruthDocumentsSection = (sectionLines, areaName, options) => {
|
|
1394
|
+
const yamlFenceRange = findTruthDocumentsYamlFenceRange(sectionLines);
|
|
1395
|
+
if (!yamlFenceRange) {
|
|
1396
|
+
return parseTruthDocumentsFromList(sectionLines, areaName, options);
|
|
1397
|
+
}
|
|
1398
|
+
const yamlResult = parseTruthDocumentsFromYaml(
|
|
1399
|
+
sectionLines,
|
|
1400
|
+
areaName,
|
|
1401
|
+
options
|
|
1940
1402
|
);
|
|
1941
|
-
if (
|
|
1942
|
-
return
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1403
|
+
if (yamlResult.diagnostics.length > 0 || yamlFenceRange.closingFenceIndex === null) {
|
|
1404
|
+
return yamlResult;
|
|
1405
|
+
}
|
|
1406
|
+
return yamlResult;
|
|
1407
|
+
};
|
|
1408
|
+
var parseAreasMarkdown = (source, options = {}) => {
|
|
1409
|
+
const lines = source.split("\n");
|
|
1410
|
+
const diagnostics = [];
|
|
1411
|
+
const areas = [];
|
|
1412
|
+
const truthDocumentReferences = [];
|
|
1413
|
+
const areaFileReferences = [];
|
|
1414
|
+
let areaIndex = 0;
|
|
1415
|
+
let currentAreaName = null;
|
|
1416
|
+
let currentSections = /* @__PURE__ */ new Map();
|
|
1417
|
+
let currentSectionName = null;
|
|
1418
|
+
const flushArea = () => {
|
|
1419
|
+
if (!currentAreaName) {
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
const truthDocumentResult = parseTruthDocumentsSection(
|
|
1423
|
+
currentSections.get("Truth documents") ?? [],
|
|
1424
|
+
currentAreaName,
|
|
1425
|
+
options
|
|
1426
|
+
);
|
|
1427
|
+
const { truthDocuments, truthDocumentEntries } = truthDocumentResult;
|
|
1428
|
+
const areaFiles = parseListSection(currentSections.get("Area files") ?? []);
|
|
1429
|
+
const codeSurface = parseListSection(
|
|
1430
|
+
currentSections.get("Code surface") ?? []
|
|
1431
|
+
);
|
|
1432
|
+
const updateTruthWhen = parseListSection(
|
|
1433
|
+
currentSections.get("Update truth when") ?? []
|
|
1434
|
+
);
|
|
1435
|
+
const areaKey = slugify(currentAreaName);
|
|
1436
|
+
const areaId = areaKey.length > 0 ? areaKey : `area-${areaIndex}`;
|
|
1437
|
+
const hasTruthDocuments = truthDocuments.length > 0;
|
|
1438
|
+
const hasAreaFiles = areaFiles.length > 0;
|
|
1439
|
+
areaIndex += 1;
|
|
1440
|
+
diagnostics.push(...truthDocumentResult.diagnostics);
|
|
1441
|
+
if (hasTruthDocuments) {
|
|
1442
|
+
truthDocumentReferences.push({
|
|
1443
|
+
id: areaId,
|
|
1444
|
+
name: currentAreaName,
|
|
1445
|
+
key: areaKey,
|
|
1446
|
+
truthDocuments,
|
|
1447
|
+
truthDocumentEntries
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
if (hasTruthDocuments === hasAreaFiles || codeSurface.length === 0 || updateTruthWhen.length === 0) {
|
|
1451
|
+
diagnostics.push(
|
|
1452
|
+
createAreaDiagnostic(
|
|
1453
|
+
`Area ${currentAreaName} must define exactly one of Truth documents or Area files, plus Code surface and Update truth when sections.`,
|
|
1454
|
+
currentAreaName
|
|
1455
|
+
)
|
|
1456
|
+
);
|
|
1457
|
+
} else if (hasAreaFiles) {
|
|
1458
|
+
areaFileReferences.push({
|
|
1459
|
+
id: areaId,
|
|
1460
|
+
name: currentAreaName,
|
|
1461
|
+
key: areaKey,
|
|
1462
|
+
areaFiles,
|
|
1463
|
+
codeSurface,
|
|
1464
|
+
updateTruthWhen
|
|
1465
|
+
});
|
|
1466
|
+
} else {
|
|
1467
|
+
areas.push({
|
|
1468
|
+
id: areaId,
|
|
1469
|
+
name: currentAreaName,
|
|
1470
|
+
key: areaKey,
|
|
1471
|
+
truthDocuments,
|
|
1472
|
+
truthDocumentEntries,
|
|
1473
|
+
codeSurface,
|
|
1474
|
+
updateTruthWhen
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
currentAreaName = null;
|
|
1478
|
+
currentSections = /* @__PURE__ */ new Map();
|
|
1479
|
+
currentSectionName = null;
|
|
1480
|
+
};
|
|
1481
|
+
for (const line of lines) {
|
|
1482
|
+
const areaHeadingMatch = line.match(/^\s{0,3}##\s+(.*)$/u);
|
|
1483
|
+
if (areaHeadingMatch) {
|
|
1484
|
+
const heading = areaHeadingMatch[1]?.trim() ?? null;
|
|
1485
|
+
flushArea();
|
|
1486
|
+
currentAreaName = heading === "Source References" ? null : heading;
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
if (!currentAreaName) {
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
if (/^(Truth documents|Area files|Code surface|Update truth when):$/u.test(
|
|
1493
|
+
line.trim()
|
|
1494
|
+
)) {
|
|
1495
|
+
currentSectionName = line.trim().slice(0, -1);
|
|
1496
|
+
currentSections.set(currentSectionName, []);
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
if (currentSectionName) {
|
|
1500
|
+
currentSections.get(currentSectionName)?.push(line);
|
|
1501
|
+
}
|
|
1948
1502
|
}
|
|
1503
|
+
flushArea();
|
|
1949
1504
|
return {
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
CONFIG_PATH2
|
|
1955
|
-
),
|
|
1956
|
-
configPath: CONFIG_PATH2
|
|
1505
|
+
areas,
|
|
1506
|
+
truthDocumentReferences,
|
|
1507
|
+
areaFileReferences,
|
|
1508
|
+
diagnostics
|
|
1957
1509
|
};
|
|
1958
1510
|
};
|
|
1959
1511
|
|
|
1960
|
-
// src/
|
|
1961
|
-
|
|
1512
|
+
// src/templates/init-files.ts
|
|
1513
|
+
import path4 from "path";
|
|
1514
|
+
import { stringify as stringify2 } from "yaml";
|
|
1515
|
+
var asRelativePath = (value) => {
|
|
1516
|
+
return value.split(path4.sep).join("/");
|
|
1517
|
+
};
|
|
1518
|
+
var currentDate = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1519
|
+
var resolveRelativePath = (fromPath, toPath) => {
|
|
1520
|
+
return asRelativePath(path4.relative(path4.dirname(fromPath), toPath));
|
|
1521
|
+
};
|
|
1522
|
+
var truthRoot = resolveEngineeringTruthRoot;
|
|
1523
|
+
var renderLaneRootReadmeSummary = (lane) => {
|
|
1524
|
+
if (lane === "product") {
|
|
1525
|
+
return [
|
|
1526
|
+
"Product truth owns capability promises, boundaries, decisions, and acceptance criteria.",
|
|
1527
|
+
"Product lane docs state what must be true, why it matters, and what success means."
|
|
1528
|
+
].join(" ");
|
|
1529
|
+
}
|
|
1962
1530
|
return [
|
|
1963
|
-
"
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
`- Claim: ${item.claim}`,
|
|
1967
|
-
` Evidence: ${item.evidence.join(" / ")}`,
|
|
1968
|
-
` Result: ${item.result}`
|
|
1969
|
-
].join("\n");
|
|
1970
|
-
})
|
|
1971
|
-
].join("\n");
|
|
1531
|
+
"Engineering truth owns current realization, contracts, architecture, workflows, operations, and tests.",
|
|
1532
|
+
"Engineering lane docs describe how the repository currently implements and operates the behavior."
|
|
1533
|
+
].join(" ");
|
|
1972
1534
|
};
|
|
1973
|
-
var
|
|
1535
|
+
var renderLaneRootLeafDocGuidance = (lane) => {
|
|
1536
|
+
if (lane === "product") {
|
|
1537
|
+
return "README.md files are indexes, not Truth Sync targets. Keep product truth in bounded capability docs.";
|
|
1538
|
+
}
|
|
1539
|
+
return "README.md files are indexes, not Truth Sync targets. Keep engineering truth in bounded behavior, contract, architecture, workflow, operations, and test docs.";
|
|
1540
|
+
};
|
|
1541
|
+
var titleCase = (value) => {
|
|
1542
|
+
return value.split(/[-_\s]+/u).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join(" ");
|
|
1543
|
+
};
|
|
1544
|
+
var renderHierarchicalAreasIndexTemplate = (config) => {
|
|
1545
|
+
const defaultArea = config.truthmark.routes.defaultArea;
|
|
1546
|
+
const childPath = `${config.truthmark.paths.routeAreasRoot}/${defaultArea}.md`;
|
|
1547
|
+
const title = titleCase(defaultArea);
|
|
1548
|
+
const sourceOfTruth = resolveRelativePath(
|
|
1549
|
+
config.truthmark.paths.routesIndex,
|
|
1550
|
+
".truthmark/config.yml"
|
|
1551
|
+
);
|
|
1974
1552
|
return [
|
|
1975
|
-
"
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
}
|
|
1553
|
+
"---",
|
|
1554
|
+
"status: active",
|
|
1555
|
+
"doc_type: route-index",
|
|
1556
|
+
`last_reviewed: ${currentDate()}`,
|
|
1557
|
+
"---",
|
|
1558
|
+
"",
|
|
1559
|
+
"# Truthmark Areas",
|
|
1560
|
+
"",
|
|
1561
|
+
`## ${title}`,
|
|
1562
|
+
"",
|
|
1563
|
+
"Area files:",
|
|
1564
|
+
`- ${childPath}`,
|
|
1565
|
+
"",
|
|
1566
|
+
"Code surface:",
|
|
1567
|
+
"- src/**",
|
|
1568
|
+
"",
|
|
1569
|
+
"Update truth when:",
|
|
1570
|
+
"- behavior changes affect the routed truth documents",
|
|
1571
|
+
"- API contracts or current feature behavior changes",
|
|
1572
|
+
"",
|
|
1573
|
+
"## Source References",
|
|
1574
|
+
"",
|
|
1575
|
+
`- ${sourceOfTruth}`,
|
|
1576
|
+
""
|
|
1984
1577
|
].join("\n");
|
|
1985
1578
|
};
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
const
|
|
1990
|
-
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
|
|
1579
|
+
var renderChildAreaTemplate = (config) => {
|
|
1580
|
+
const defaultArea = config.truthmark.routes.defaultArea;
|
|
1581
|
+
const title = titleCase(defaultArea);
|
|
1582
|
+
const truthDocsRoot = truthRoot(config);
|
|
1583
|
+
const bootstrapTruthDoc = `${truthDocsRoot}/${defaultArea}/bootstrap-routing.md`;
|
|
1584
|
+
const templatePath = `${config.truthmark.paths.routeAreasRoot}/${defaultArea}.md`;
|
|
1585
|
+
const sourceOfTruth = resolveRelativePath(
|
|
1586
|
+
templatePath,
|
|
1587
|
+
".truthmark/config.yml"
|
|
1588
|
+
);
|
|
1589
|
+
return [
|
|
1590
|
+
"---",
|
|
1591
|
+
"status: active",
|
|
1592
|
+
"doc_type: area-route",
|
|
1593
|
+
`last_reviewed: ${currentDate()}`,
|
|
1594
|
+
"---",
|
|
1595
|
+
"",
|
|
1596
|
+
`# ${title} Areas`,
|
|
1597
|
+
"",
|
|
1598
|
+
`## ${title}`,
|
|
1599
|
+
"",
|
|
1600
|
+
"Truth documents:",
|
|
1601
|
+
"```yaml",
|
|
1602
|
+
"truth_documents:",
|
|
1603
|
+
` - path: ${bootstrapTruthDoc}`,
|
|
1604
|
+
" kind: engineering-workflow",
|
|
1605
|
+
" lane: engineering",
|
|
1606
|
+
"```",
|
|
1607
|
+
"",
|
|
1608
|
+
"This is a provisional bootstrap route. It exists only to make fresh repositories routeable until real product, service, domain, or ownership areas are created.",
|
|
1609
|
+
"",
|
|
1610
|
+
"Code surface:",
|
|
1611
|
+
"- src/**",
|
|
1612
|
+
"",
|
|
1613
|
+
"Update truth when:",
|
|
1614
|
+
"- this provisional bootstrap route is the only match for a real code surface",
|
|
1615
|
+
"- route ownership is still broad, mixed, or ambiguous",
|
|
1616
|
+
"- Run Truth Structure before normal Truth Sync so the touched code gets a bounded owner",
|
|
1617
|
+
"",
|
|
1618
|
+
"## Source References",
|
|
1619
|
+
"",
|
|
1620
|
+
`- ${sourceOfTruth}`,
|
|
1621
|
+
""
|
|
1622
|
+
].join("\n");
|
|
1994
1623
|
};
|
|
1995
|
-
var
|
|
1996
|
-
const
|
|
1997
|
-
|
|
1624
|
+
var renderBootstrapRoutingDocTemplate = (config) => {
|
|
1625
|
+
const defaultArea = config.truthmark.routes.defaultArea;
|
|
1626
|
+
const title = titleCase(defaultArea);
|
|
1627
|
+
const templatePath = `${truthRoot(config)}/${defaultArea}/bootstrap-routing.md`;
|
|
1628
|
+
const routePath = `${config.truthmark.paths.routeAreasRoot}/${defaultArea}.md`;
|
|
1629
|
+
const routeSource = resolveRelativePath(templatePath, routePath);
|
|
1630
|
+
const configSource = resolveRelativePath(
|
|
1631
|
+
templatePath,
|
|
1632
|
+
".truthmark/config.yml"
|
|
1633
|
+
);
|
|
1634
|
+
const today = currentDate();
|
|
1635
|
+
return [
|
|
1636
|
+
"---",
|
|
1637
|
+
"status: active",
|
|
1638
|
+
"truth_kind: engineering-workflow",
|
|
1639
|
+
`last_reviewed: ${today}`,
|
|
1640
|
+
"---",
|
|
1641
|
+
"",
|
|
1642
|
+
`# ${title} Bootstrap Routing`,
|
|
1643
|
+
"",
|
|
1644
|
+
"## Purpose",
|
|
1645
|
+
"",
|
|
1646
|
+
`This doc records the provisional broad route for ${defaultArea}.`,
|
|
1647
|
+
"This doc is a bootstrap handoff, not a behavior truth dumping ground.",
|
|
1648
|
+
"It is not a substitute for bounded product and engineering truth docs.",
|
|
1649
|
+
"",
|
|
1650
|
+
"## Scope",
|
|
1651
|
+
"",
|
|
1652
|
+
"This doc owns only the initial routing workflow for a fresh Truthmark repository whose default route still maps a broad code surface such as `src/**`.",
|
|
1653
|
+
"It does not own implementation behavior under that code surface.",
|
|
1654
|
+
"",
|
|
1655
|
+
"## Current Implementation Behavior",
|
|
1656
|
+
"",
|
|
1657
|
+
"The scaffold creates this provisional bootstrap handoff only when a default broad route needs a canonical owner. Agents use it as a signal to run Truth Structure and create bounded routes before normal Truth Sync, not as a place to accumulate implementation claims.",
|
|
1658
|
+
"",
|
|
1659
|
+
"## Product Truth Links",
|
|
1660
|
+
"",
|
|
1661
|
+
"- None. This is an engineering bootstrap handoff for routing setup, not a product promise.",
|
|
1662
|
+
"",
|
|
1663
|
+
"## Triggers",
|
|
1664
|
+
"",
|
|
1665
|
+
"- A real code change maps only to this provisional broad route.",
|
|
1666
|
+
"- Truth Sync cannot identify a specific behavior-owned route and bounded truth owner.",
|
|
1667
|
+
"- A maintainer or agent is onboarding the first real product, service, domain, package, or ownership area.",
|
|
1668
|
+
"",
|
|
1669
|
+
"## Inputs",
|
|
1670
|
+
"",
|
|
1671
|
+
"- Current route files under the configured Truthmark route root.",
|
|
1672
|
+
"- The touched code, tests, configuration, and existing docs needed to infer the smallest real owner.",
|
|
1673
|
+
"- Repository instruction files that exist in the checkout.",
|
|
1674
|
+
"",
|
|
1675
|
+
"## Execution Model",
|
|
1676
|
+
"",
|
|
1677
|
+
"Run Truth Structure before normal Truth Sync when real code changes touch only this broad route. Truth Structure should create or repair bounded areas first; Truth Sync should then update the bounded owner docs.",
|
|
1678
|
+
"",
|
|
1679
|
+
"## Steps",
|
|
1680
|
+
"",
|
|
1681
|
+
"1. Treat this route as provisional and insufficient for normal behavior maintenance.",
|
|
1682
|
+
"2. Inspect the touched code/test surface and infer the narrowest durable owner.",
|
|
1683
|
+
"3. Create or repair route entries and truth docs for that owner.",
|
|
1684
|
+
"4. Leave this bootstrap doc small; do not append behavior details here.",
|
|
1685
|
+
"5. Resume Truth Sync only after the touched code resolves to a bounded owner.",
|
|
1686
|
+
"",
|
|
1687
|
+
"## State, Retry, And Failure Behavior",
|
|
1688
|
+
"",
|
|
1689
|
+
"If ownership cannot be inferred safely, stop and report manual-review files instead of widening this route or adding generic behavior prose.",
|
|
1690
|
+
"",
|
|
1691
|
+
"## Outputs",
|
|
1692
|
+
"",
|
|
1693
|
+
"- Bounded route areas and lane-appropriate truth docs for the touched surface.",
|
|
1694
|
+
"- A compact manual handoff report when ownership remains ambiguous.",
|
|
1695
|
+
"",
|
|
1696
|
+
"## Engineering Decisions",
|
|
1697
|
+
"",
|
|
1698
|
+
`- Decision (${today}): Default broad routing is provisional bootstrap state. Agents should create bounded areas before normal Truth Sync rather than extending a catch-all overview doc.`,
|
|
1699
|
+
"",
|
|
1700
|
+
"## Rationale",
|
|
1701
|
+
"",
|
|
1702
|
+
"Scoped ownership keeps agent context close to affected files and prevents broad default docs from absorbing unrelated behavior. This preserves agent-native truth maintenance without adding a token-heavy discovery layer.",
|
|
1703
|
+
"",
|
|
1704
|
+
"## Non-Goals",
|
|
1705
|
+
"",
|
|
1706
|
+
"- This doc is not a repository behavior overview.",
|
|
1707
|
+
"- This doc is not a product capability or engineering behavior owner.",
|
|
1708
|
+
"- This doc is not a permanent home for claims about files under `src/**`.",
|
|
1709
|
+
"",
|
|
1710
|
+
"## Maintenance Notes",
|
|
1711
|
+
"",
|
|
1712
|
+
"Keep this doc short. When a repository has real bounded routes, prefer updating those routes and their truth docs instead of expanding this bootstrap handoff.",
|
|
1713
|
+
"",
|
|
1714
|
+
"## Source References",
|
|
1715
|
+
"",
|
|
1716
|
+
`- ${routeSource}`,
|
|
1717
|
+
`- ${configSource}`,
|
|
1718
|
+
""
|
|
1719
|
+
].join("\n");
|
|
1998
1720
|
};
|
|
1999
|
-
var
|
|
2000
|
-
const
|
|
2001
|
-
const
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
"classify the request or changed surface as product-lane, engineering-lane, both-lane, or ambiguous for reporting only",
|
|
2005
|
-
`product-lane ownership belongs under ${productTruthRoot} and describes product promises, boundaries, rationale, decisions, and success criteria`,
|
|
2006
|
-
`engineering-lane ownership belongs under ${engineeringTruthRoot} and describes source-backed current realization, contracts, architecture, workflows, operations, or tests`,
|
|
2007
|
-
"both-lane ownership uses separate product and engineering docs cross-linked in route YAML with realized_by and realizes, not in doc frontmatter",
|
|
2008
|
-
"ambiguous lane ownership should be reported for manual handoff or routed to Truth Structure",
|
|
2009
|
-
LANE_INVARIANT
|
|
2010
|
-
].join("\n"),
|
|
2011
|
-
indent
|
|
1721
|
+
var renderTruthRootReadmeTemplate = (config = createDefaultConfig(), lane = "engineering") => {
|
|
1722
|
+
const templatePath = `${lane === "product" ? resolveProductTruthRoot(config) : resolveEngineeringTruthRoot(config)}/README.md`;
|
|
1723
|
+
const sourceOfTruth = resolveRelativePath(
|
|
1724
|
+
templatePath,
|
|
1725
|
+
config.truthmark.paths.routesIndex
|
|
2012
1726
|
);
|
|
2013
|
-
};
|
|
2014
|
-
var DECISION_TRUTH_INSTRUCTIONS = [
|
|
2015
|
-
"Decision truth lives in the canonical doc it governs; date active decisions inline when added or changed.",
|
|
2016
|
-
"Do not create separate active-decision ADR/planning logs; replace the active decision and let Git history carry the audit trail.",
|
|
2017
|
-
"Product decisions belong in product truth; engineering, architecture, contract, workflow, and operational decisions belong in engineering truth."
|
|
2018
|
-
].join("\n");
|
|
2019
|
-
var LANE_INVARIANT = "Do not make product docs a summary of engineering docs. Do not make engineering docs a detailed version of product docs. Product truth says what must be true and why. Engineering truth says how the repository currently realizes it.";
|
|
2020
|
-
var renderLaneClassificationInstructions = (config = defaultAgentConfig()) => {
|
|
2021
|
-
const productTruthRoot = resolveProductTruthRoot(config);
|
|
2022
|
-
const engineeringTruthRoot = resolveEngineeringTruthRoot(config);
|
|
2023
1727
|
return [
|
|
2024
|
-
"
|
|
2025
|
-
"
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
"
|
|
2029
|
-
"
|
|
2030
|
-
|
|
1728
|
+
"---",
|
|
1729
|
+
"status: active",
|
|
1730
|
+
"doc_type: index",
|
|
1731
|
+
`last_reviewed: ${currentDate()}`,
|
|
1732
|
+
"---",
|
|
1733
|
+
"",
|
|
1734
|
+
"# Truth Docs",
|
|
1735
|
+
"",
|
|
1736
|
+
"This directory is an index for current truth docs organized by the configured Truthmark hierarchy.",
|
|
1737
|
+
"",
|
|
1738
|
+
renderLaneRootReadmeSummary(lane),
|
|
1739
|
+
"",
|
|
1740
|
+
renderLaneRootLeafDocGuidance(lane),
|
|
1741
|
+
"",
|
|
1742
|
+
"## Source References",
|
|
1743
|
+
"",
|
|
1744
|
+
`- ${sourceOfTruth}`,
|
|
1745
|
+
""
|
|
2031
1746
|
].join("\n");
|
|
2032
1747
|
};
|
|
2033
|
-
var
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
].join("\n");
|
|
2042
|
-
var FEATURE_DOC_TEMPLATE_INSTRUCTIONS = [
|
|
2043
|
-
"When creating or updating a truth doc, inspect the routed truth kind and use the matching template under the configured Truthmark templates root.",
|
|
2044
|
-
"Supported kinds: product-capability, engineering-behavior, engineering-contract, engineering-architecture, engineering-workflow, engineering-operations, and engineering-test-behavior.",
|
|
2045
|
-
"Treat the HTML comments under each template section as normative authoring guidance for that section.",
|
|
2046
|
-
"Align existing docs to that template and write or repair section content so it satisfies the comment guidance while preserving accurate authored content.",
|
|
2047
|
-
"If the template is missing, use lane-specific sections: product truth says what must be true and why; engineering truth says how the repository currently realizes it.",
|
|
2048
|
-
"Teams may edit template files under the configured Truthmark templates root to define their local truth-doc standards."
|
|
2049
|
-
].join("\n");
|
|
2050
|
-
var TRUTH_DOC_AUTHORING_STYLE_INSTRUCTIONS = [
|
|
2051
|
-
"Truth-doc prose style:",
|
|
2052
|
-
"- Use professional, plain technical prose. Prefer specific current-state claims over promotional, symbolic, or generic significance language.",
|
|
2053
|
-
"- Avoid common AI-writing tells: pivotal, crucial, underscores, serves as, stands as, showcases, landscape, vague expert attributions, and generic upbeat conclusions.",
|
|
2054
|
-
"- Keep claims evidence-backed and diff-friendly: one durable claim per bullet or line; paragraphs should be no longer than one or two short sentences.",
|
|
2055
|
-
"- Do not add personality, rhetorical flourish, first-person commentary, or marketing tone.",
|
|
2056
|
-
"- Rewrite dense or formulaic prose only when it improves readability without removing scope, evidence, decisions, or source references."
|
|
2057
|
-
].join("\n");
|
|
2058
|
-
var renderTruthDocOwnershipGateSection = (subject, outcome) => {
|
|
1748
|
+
var renderTruthDomainReadmeTemplate = (config) => {
|
|
1749
|
+
const defaultArea = config.truthmark.routes.defaultArea;
|
|
1750
|
+
const title = titleCase(defaultArea);
|
|
1751
|
+
const templatePath = `${truthRoot(config)}/${defaultArea}/README.md`;
|
|
1752
|
+
const sourceOfTruth = resolveRelativePath(
|
|
1753
|
+
templatePath,
|
|
1754
|
+
`${config.truthmark.paths.routeAreasRoot}/${defaultArea}.md`
|
|
1755
|
+
);
|
|
2059
1756
|
return [
|
|
2060
|
-
"
|
|
2061
|
-
|
|
2062
|
-
"
|
|
2063
|
-
|
|
2064
|
-
"
|
|
1757
|
+
"---",
|
|
1758
|
+
"status: active",
|
|
1759
|
+
"doc_type: index",
|
|
1760
|
+
`last_reviewed: ${currentDate()}`,
|
|
1761
|
+
"---",
|
|
1762
|
+
"",
|
|
1763
|
+
`# ${title} Truth Docs`,
|
|
1764
|
+
"",
|
|
1765
|
+
`This directory indexes bounded ${title.toLowerCase()} truth docs.`,
|
|
1766
|
+
"",
|
|
1767
|
+
"README.md files are indexes, not Truth Sync targets. Keep bounded truth in leaf docs in this directory.",
|
|
1768
|
+
"",
|
|
1769
|
+
"Current leaf docs:",
|
|
1770
|
+
"",
|
|
1771
|
+
"- [Bootstrap routing](bootstrap-routing.md)",
|
|
1772
|
+
"",
|
|
1773
|
+
"## Source References",
|
|
1774
|
+
"",
|
|
1775
|
+
`- ${sourceOfTruth}`,
|
|
1776
|
+
""
|
|
2065
1777
|
].join("\n");
|
|
2066
1778
|
};
|
|
2067
|
-
var
|
|
2068
|
-
"Decision/Rationale preservation review:",
|
|
2069
|
-
"- before any truth-doc split, restructure, or shape repair, inventory existing Product Decisions, Engineering Decisions, and Rationale sections in every source or touched truth doc",
|
|
2070
|
-
"- preserve each current decision and rationale in the correct product or engineering lane owner; when splitting, move it to the new owner doc rather than deleting it or leaving it in an index",
|
|
2071
|
-
"- remove or narrow a decision or rationale only when checkout evidence shows it is stale or unsupported, and report the exact claim, evidence, and result",
|
|
2072
|
-
"- if ownership of a decision or rationale is unclear, stop with manual-review files instead of deleting it or guessing",
|
|
2073
|
-
"- after the edit, verify every touched truth doc keeps lane-appropriate decision/rationale sections and every pre-existing entry is preserved, moved, narrowed, removed with evidence, or blocked"
|
|
2074
|
-
].join("\n");
|
|
2075
|
-
var renderTruthDocRestructureGateSection = (scope) => {
|
|
1779
|
+
var renderTemplateSection = (section) => {
|
|
2076
1780
|
return [
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
"
|
|
2080
|
-
|
|
2081
|
-
"
|
|
2082
|
-
"
|
|
2083
|
-
|
|
2084
|
-
|
|
1781
|
+
section.heading,
|
|
1782
|
+
"",
|
|
1783
|
+
"<!--",
|
|
1784
|
+
...section.guidance,
|
|
1785
|
+
"-->",
|
|
1786
|
+
"",
|
|
1787
|
+
`{{${section.placeholder}}}`,
|
|
1788
|
+
""
|
|
1789
|
+
];
|
|
2085
1790
|
};
|
|
2086
|
-
var
|
|
2087
|
-
|
|
2088
|
-
"Keep ordinary behavior, endpoints, UI copy, validation rules, and bug fixes in behavior or contract docs unless they change those boundaries."
|
|
2089
|
-
].join("\n");
|
|
2090
|
-
var renderRouteFirstEvidenceGateSection = (subject, noImpactedDocOutcome) => {
|
|
2091
|
-
return [
|
|
2092
|
-
"Evidence checklist:",
|
|
2093
|
-
`- route-first: map ${subject} to bounded route owners and primary canonical docs`,
|
|
2094
|
-
"- review new or changed behavior-bearing claims only in touched docs, route ownership, lane-specific decisions, and rationale",
|
|
2095
|
-
"- support claims with primary checkout evidence: implementation, config, routing, generated templates, schemas, or contract definitions",
|
|
2096
|
-
"- tests/examples/canonical docs corroborate; they are not sole proof when implementation conflicts",
|
|
2097
|
-
"- remove, narrow, or record unsupported claims for manual handoff",
|
|
2098
|
-
`- ${noImpactedDocOutcome}`
|
|
2099
|
-
].join("\n");
|
|
1791
|
+
var titleToPlaceholder = (title) => {
|
|
1792
|
+
return title.replace(/^#+\s+/u, "").toLowerCase().replaceAll(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
2100
1793
|
};
|
|
2101
|
-
var
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
1794
|
+
var findTemplateSectionHeadings = (template) => {
|
|
1795
|
+
const matches = [];
|
|
1796
|
+
let fencedCodeMarker = null;
|
|
1797
|
+
let fencedCodeLength = 0;
|
|
1798
|
+
for (const lineMatch of template.matchAll(/^.*(?:\r?\n|$)/gm)) {
|
|
1799
|
+
const rawLine = lineMatch[0];
|
|
1800
|
+
if (rawLine.length === 0) {
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1803
|
+
const line = rawLine.replace(/\r?\n$/u, "");
|
|
1804
|
+
const fenceMatch = /^(?: {0,3})(`{3,}|~{3,})/u.exec(line);
|
|
1805
|
+
if (fenceMatch) {
|
|
1806
|
+
const marker = fenceMatch[1]?.[0];
|
|
1807
|
+
const length = fenceMatch[1]?.length ?? 0;
|
|
1808
|
+
if (fencedCodeMarker === null) {
|
|
1809
|
+
fencedCodeMarker = marker;
|
|
1810
|
+
fencedCodeLength = length;
|
|
1811
|
+
} else if (marker === fencedCodeMarker && length >= fencedCodeLength) {
|
|
1812
|
+
fencedCodeMarker = null;
|
|
1813
|
+
fencedCodeLength = 0;
|
|
1814
|
+
}
|
|
1815
|
+
continue;
|
|
1816
|
+
}
|
|
1817
|
+
if (fencedCodeMarker === null && /^## .+$/u.test(line)) {
|
|
1818
|
+
matches.push({ heading: line.trim(), index: lineMatch.index });
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return matches;
|
|
2108
1822
|
};
|
|
2109
|
-
var
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
1823
|
+
var parseTemplateSections = (template) => {
|
|
1824
|
+
const matches = findTemplateSectionHeadings(template);
|
|
1825
|
+
if (matches.length === 0) {
|
|
1826
|
+
return { preamble: template.trimEnd(), sections: [] };
|
|
1827
|
+
}
|
|
1828
|
+
const sections = matches.map((match, index) => {
|
|
1829
|
+
const start = match.index;
|
|
1830
|
+
const next = matches[index + 1];
|
|
1831
|
+
const end = next?.index ?? template.length;
|
|
1832
|
+
return {
|
|
1833
|
+
heading: match.heading,
|
|
1834
|
+
block: template.slice(start, end).trimEnd()
|
|
1835
|
+
};
|
|
1836
|
+
});
|
|
1837
|
+
return {
|
|
1838
|
+
preamble: template.slice(0, matches[0]?.index ?? 0).trimEnd(),
|
|
1839
|
+
sections
|
|
1840
|
+
};
|
|
2116
1841
|
};
|
|
2117
|
-
var
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
] : [];
|
|
2124
|
-
const readOnlyScope = writeAgents.length > 0 ? "for verification" : "only";
|
|
2125
|
-
const readOnlyWorkerLabel = writeAgents.length > 0 ? "read-only workers" : "workers";
|
|
2126
|
-
return [
|
|
2127
|
-
"Codex subagent mode:",
|
|
2128
|
-
"- use automatically when this workflow runs in Codex and the parent agent chooses bounded subagent fan-out",
|
|
2129
|
-
`- dispatch read-only project agents ${readOnlyScope}: ${agents.join(", ")}`,
|
|
2130
|
-
`- ${readOnlyWorkerLabel} inspect checkout evidence directly, return structured findings, and must not edit files`,
|
|
2131
|
-
`- parent supplies bounded evidence shards; ${readOnlyWorkerLabel} must not preload host instruction files or repo-wide policy docs unless assigned as evidence`,
|
|
2132
|
-
...writeAgentLines,
|
|
2133
|
-
`- ${parentRule}`
|
|
2134
|
-
].join("\n");
|
|
1842
|
+
var LEGACY_MANAGED_TEMPLATE_HEADINGS = /* @__PURE__ */ new Map([
|
|
1843
|
+
["## Current Behavior", "## Current Implementation Behavior"],
|
|
1844
|
+
["## Source Evidence", "## Source References"]
|
|
1845
|
+
]);
|
|
1846
|
+
var resolveManagedTemplateHeading = (heading) => {
|
|
1847
|
+
return LEGACY_MANAGED_TEMPLATE_HEADINGS.get(heading) ?? heading;
|
|
2135
1848
|
};
|
|
2136
|
-
var
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
1849
|
+
var stripManagedFrontmatterFields = (preamble) => {
|
|
1850
|
+
if (!preamble.startsWith("---\n")) {
|
|
1851
|
+
return preamble;
|
|
1852
|
+
}
|
|
1853
|
+
const lines = preamble.split("\n");
|
|
1854
|
+
const closingIndex = lines.findIndex(
|
|
1855
|
+
(line, index) => index > 0 && line.trim() === "---"
|
|
2140
1856
|
);
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
const
|
|
2148
|
-
|
|
1857
|
+
if (closingIndex < 0) {
|
|
1858
|
+
return preamble;
|
|
1859
|
+
}
|
|
1860
|
+
const fieldsToRemove = /* @__PURE__ */ new Set(["source_of_truth", "doc_type", "truth_lane"]);
|
|
1861
|
+
const keptFrontmatterLines = [];
|
|
1862
|
+
let skippingManagedField = false;
|
|
1863
|
+
for (const line of lines.slice(1, closingIndex)) {
|
|
1864
|
+
const keyMatch = /^([A-Za-z0-9_-]+):(\s|$)/u.exec(line);
|
|
1865
|
+
if (keyMatch) {
|
|
1866
|
+
skippingManagedField = fieldsToRemove.has(keyMatch[1] ?? "");
|
|
1867
|
+
}
|
|
1868
|
+
if (!skippingManagedField) {
|
|
1869
|
+
keptFrontmatterLines.push(line);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
2149
1872
|
return [
|
|
2150
|
-
"
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
...writeAgentLines,
|
|
2156
|
-
`- ${parentRule}`
|
|
2157
|
-
].join("\n");
|
|
1873
|
+
"---",
|
|
1874
|
+
...keptFrontmatterLines,
|
|
1875
|
+
"---",
|
|
1876
|
+
...lines.slice(closingIndex + 1)
|
|
1877
|
+
].join("\n").trimEnd();
|
|
2158
1878
|
};
|
|
2159
|
-
var
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
const
|
|
2164
|
-
|
|
1879
|
+
var mergeTruthDocTemplate = (existingTemplate, defaultTemplate) => {
|
|
1880
|
+
if (existingTemplate.trim().length === 0) {
|
|
1881
|
+
return defaultTemplate;
|
|
1882
|
+
}
|
|
1883
|
+
const defaultParsed = parseTemplateSections(defaultTemplate);
|
|
1884
|
+
const existingParsed = parseTemplateSections(existingTemplate);
|
|
1885
|
+
const defaultHeadings = new Set(
|
|
1886
|
+
defaultParsed.sections.map((section) => section.heading)
|
|
2165
1887
|
);
|
|
2166
|
-
const
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
1888
|
+
const customBeforeDefault = /* @__PURE__ */ new Map();
|
|
1889
|
+
const trailingCustomSections = [];
|
|
1890
|
+
existingParsed.sections.forEach((section, index) => {
|
|
1891
|
+
if (defaultHeadings.has(resolveManagedTemplateHeading(section.heading))) {
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
const nextDefaultSection = existingParsed.sections.slice(index + 1).find(
|
|
1895
|
+
(candidate) => defaultHeadings.has(resolveManagedTemplateHeading(candidate.heading))
|
|
1896
|
+
);
|
|
1897
|
+
if (nextDefaultSection) {
|
|
1898
|
+
const nextDefaultHeading = resolveManagedTemplateHeading(
|
|
1899
|
+
nextDefaultSection.heading
|
|
1900
|
+
);
|
|
1901
|
+
const bucket = customBeforeDefault.get(nextDefaultHeading) ?? [];
|
|
1902
|
+
bucket.push(section);
|
|
1903
|
+
customBeforeDefault.set(nextDefaultHeading, bucket);
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
trailingCustomSections.push(section);
|
|
1907
|
+
});
|
|
1908
|
+
const mergedSections = defaultParsed.sections.flatMap((section) => [
|
|
1909
|
+
...customBeforeDefault.get(section.heading) ?? [],
|
|
1910
|
+
section
|
|
1911
|
+
]);
|
|
2174
1912
|
return [
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
...writeAgentLines,
|
|
2181
|
-
`- ${parentRule}`
|
|
2182
|
-
].join("\n");
|
|
1913
|
+
stripManagedFrontmatterFields(existingParsed.preamble),
|
|
1914
|
+
...mergedSections.map((section) => section.block),
|
|
1915
|
+
...trailingCustomSections.map((section) => section.block),
|
|
1916
|
+
""
|
|
1917
|
+
].filter((block) => block.length > 0).join("\n\n");
|
|
2183
1918
|
};
|
|
2184
|
-
var
|
|
2185
|
-
const mentions = agents.map((agent) => `@${agent.replace(/_/gu, "-")}`);
|
|
2186
|
-
const writeMentions = writeAgents.map(
|
|
2187
|
-
(agent) => `@${agent.replace(/_/gu, "-")}`
|
|
2188
|
-
);
|
|
2189
|
-
const writeAgentLines = writeMentions.length > 0 ? [
|
|
2190
|
-
`- dispatch write-capable project custom agents only with explicit write leases: ${writeMentions.join(", ")}`,
|
|
2191
|
-
"- each write lease must name objective, required reads, allowed writes, forbidden writes, evidence, verification, and report fields",
|
|
2192
|
-
"- write workers must stop when a required edit is off-lease and report status, filesChanged, evidence, offLeaseChanges, blockers, and notes",
|
|
2193
|
-
"- parent must inspect the actual checkout diff against each lease before accepting a worker report"
|
|
2194
|
-
] : [];
|
|
2195
|
-
const readOnlyScope = writeAgents.length > 0 ? "for verification" : "only";
|
|
2196
|
-
const readOnlyCustomAgentLabel = writeAgents.length > 0 ? "read-only custom agents" : "custom agents";
|
|
1919
|
+
var renderBehaviorDocTemplateFile = () => {
|
|
2197
1920
|
return [
|
|
2198
|
-
"
|
|
2199
|
-
"
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
1921
|
+
"---",
|
|
1922
|
+
"status: active",
|
|
1923
|
+
"truth_kind: engineering-behavior",
|
|
1924
|
+
`last_reviewed: ${currentDate()}`,
|
|
1925
|
+
"---",
|
|
1926
|
+
"",
|
|
1927
|
+
"# {{title}}",
|
|
1928
|
+
"",
|
|
1929
|
+
"## Purpose",
|
|
1930
|
+
"",
|
|
1931
|
+
"<!--",
|
|
1932
|
+
"State the user/system outcome this behavior protects and why it exists.",
|
|
1933
|
+
"Include the problem boundary and durable value; exclude roadmap, implementation plan, and historical narrative.",
|
|
1934
|
+
"List the code, config, docs, or tests that support the claim in Source References rather than prose-only assertion.",
|
|
1935
|
+
"-->",
|
|
1936
|
+
"",
|
|
1937
|
+
"{{purpose}}",
|
|
1938
|
+
"",
|
|
1939
|
+
"## Scope",
|
|
1940
|
+
"",
|
|
1941
|
+
"<!--",
|
|
1942
|
+
"Define the one coherent behavior surface this document owns.",
|
|
1943
|
+
"Include in-scope actors, entrypoints, state/data owned by this doc, and explicit handoffs to neighboring truth docs.",
|
|
1944
|
+
"Split into another leaf doc when content introduces a distinct outcome, state machine, rule family, external contract, or route owner.",
|
|
1945
|
+
"Keep README.md files as indexes only.",
|
|
1946
|
+
"-->",
|
|
1947
|
+
"",
|
|
1948
|
+
"{{scope}}",
|
|
1949
|
+
"",
|
|
1950
|
+
"This doc was created from the editable engineering-behavior template at {{template_path}}.",
|
|
1951
|
+
"",
|
|
1952
|
+
"## Current Implementation Behavior",
|
|
1953
|
+
"",
|
|
1954
|
+
"<!--",
|
|
1955
|
+
"Describe only current implemented behavior in present tense.",
|
|
1956
|
+
"Cover observable behavior, important defaults, and user/system-visible effects; exclude desired future behavior and speculative design.",
|
|
1957
|
+
"Every non-obvious claim should be checkable from Source References.",
|
|
1958
|
+
"-->",
|
|
1959
|
+
"",
|
|
1960
|
+
"{{current_implementation_behavior}}",
|
|
1961
|
+
"",
|
|
1962
|
+
"## Core Rules",
|
|
1963
|
+
"",
|
|
1964
|
+
"<!--",
|
|
1965
|
+
"Capture stable business rules, invariants, precedence rules, validation rules, and must-never constraints.",
|
|
1966
|
+
"Separate rules from incidental implementation details; cite current implementation or tests for rule enforcement.",
|
|
1967
|
+
"-->",
|
|
1968
|
+
"",
|
|
1969
|
+
"{{core_rules}}",
|
|
1970
|
+
"",
|
|
1971
|
+
"## Behavior Scenarios",
|
|
1972
|
+
"",
|
|
1973
|
+
"<!--",
|
|
1974
|
+
"Use compact scenario blocks only where they clarify normal, fallback, or compatibility-critical behavior.",
|
|
1975
|
+
"Write scenarios as current truth, not desired requirements: `#### Scenario: <implemented case>` followed by `- **GIVEN** ...`, `- **WHEN** ...`, `- **THEN** ...`, and optional `- **AND** ...` bullets.",
|
|
1976
|
+
"Keep each bullet evidence-backed and observable; do not force a scenario for every rule.",
|
|
1977
|
+
"-->",
|
|
1978
|
+
"",
|
|
1979
|
+
"{{behavior_scenarios}}",
|
|
1980
|
+
"",
|
|
1981
|
+
"## Flows And States",
|
|
1982
|
+
"",
|
|
1983
|
+
"<!--",
|
|
1984
|
+
"Document state transitions, lifecycle stages, retries, fallbacks, route switches, and important error paths.",
|
|
1985
|
+
"State 'None beyond current behavior.' when this behavior has no distinct flow or state model.",
|
|
1986
|
+
"-->",
|
|
1987
|
+
"",
|
|
1988
|
+
"{{flows_and_states}}",
|
|
1989
|
+
"",
|
|
1990
|
+
"## Contracts",
|
|
1991
|
+
"",
|
|
1992
|
+
"<!--",
|
|
1993
|
+
"Capture user-visible or integration contracts: CLI/API shape, inputs, outputs, diagnostics, files, events, permissions, or links to canonical contract docs.",
|
|
1994
|
+
"Avoid duplicating a separate canonical contract doc; link to it when contract ownership lives elsewhere.",
|
|
1995
|
+
"-->",
|
|
1996
|
+
"",
|
|
1997
|
+
"{{contracts}}",
|
|
1998
|
+
"",
|
|
1999
|
+
"## Product Truth Links",
|
|
2000
|
+
"",
|
|
2001
|
+
"<!--",
|
|
2002
|
+
"List product truth docs this engineering doc realizes; author canonical realizes links in route YAML, not doc frontmatter.",
|
|
2003
|
+
"Use 'None.' when this is purely internal engineering behavior.",
|
|
2004
|
+
"-->",
|
|
2005
|
+
"",
|
|
2006
|
+
"{{product_truth_links}}",
|
|
2007
|
+
"",
|
|
2008
|
+
"## Engineering Decisions",
|
|
2009
|
+
"",
|
|
2010
|
+
"<!--",
|
|
2011
|
+
"Keep active decisions only, dated inline when added or changed.",
|
|
2012
|
+
"Explain decisions that shape behavior, boundaries, rejected alternatives, or migration constraints; replace stale decisions instead of appending historical logs.",
|
|
2013
|
+
"-->",
|
|
2014
|
+
"",
|
|
2015
|
+
"{{engineering_decisions}}",
|
|
2016
|
+
"",
|
|
2017
|
+
"## Rationale",
|
|
2018
|
+
"",
|
|
2019
|
+
"<!--",
|
|
2020
|
+
"Explain why the current behavior and active decisions are this way, including tradeoffs and constraints.",
|
|
2021
|
+
"Tie rationale to evidence-backed behavior; do not use this as a changelog.",
|
|
2022
|
+
"-->",
|
|
2023
|
+
"",
|
|
2024
|
+
"{{rationale}}",
|
|
2025
|
+
"",
|
|
2026
|
+
"## Non-Goals",
|
|
2027
|
+
"",
|
|
2028
|
+
"<!--",
|
|
2029
|
+
"Name adjacent behavior this doc intentionally does not own, especially tempting future expansions or neighboring route owners.",
|
|
2030
|
+
"Use this section to prevent scope creep and duplicate truth ownership.",
|
|
2031
|
+
"-->",
|
|
2032
|
+
"",
|
|
2033
|
+
"{{non_goals}}",
|
|
2034
|
+
"",
|
|
2035
|
+
"## Maintenance Notes",
|
|
2036
|
+
"",
|
|
2037
|
+
"<!--",
|
|
2038
|
+
"List related tests, routing cautions, migration notes, evidence drift risks, and review triggers for future maintainers or agents.",
|
|
2039
|
+
"Keep this operational and current-state focused, not historical.",
|
|
2040
|
+
"-->",
|
|
2041
|
+
"",
|
|
2042
|
+
"{{maintenance_notes}}",
|
|
2043
|
+
"",
|
|
2044
|
+
"## Source References",
|
|
2045
|
+
"",
|
|
2046
|
+
"<!--",
|
|
2047
|
+
"List source files, tests, configs, generated templates, route files, or product instructions that support current claims.",
|
|
2048
|
+
"-->",
|
|
2049
|
+
"",
|
|
2050
|
+
"{{source_references}}",
|
|
2051
|
+
""
|
|
2205
2052
|
].join("\n");
|
|
2206
2053
|
};
|
|
2207
|
-
var
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2054
|
+
var sectionSpec = (heading, guidance, placeholder = titleToPlaceholder(heading)) => ({ heading, guidance, placeholder });
|
|
2055
|
+
var PURPOSE_SECTION = sectionSpec("## Purpose", [
|
|
2056
|
+
"State the software-engineering outcome this document protects and why the documented surface exists.",
|
|
2057
|
+
"Include durable value, impacted users/systems, and the problem boundary; exclude roadmap, implementation plans, and historical narrative.",
|
|
2058
|
+
"Keep claims traceable to Source References rather than prose-only assertion."
|
|
2059
|
+
]);
|
|
2060
|
+
var SCOPE_SECTION = sectionSpec("## Scope", [
|
|
2061
|
+
"Define the one coherent surface this document owns, including actors, entrypoints, owned state/data, and handoffs to neighboring truth docs.",
|
|
2062
|
+
"Call out important out-of-scope boundaries here or in Non-Goals; split the doc when it mixes distinct outcomes, lifecycles, contracts, or owners."
|
|
2063
|
+
]);
|
|
2064
|
+
var PRODUCT_DECISIONS_SECTION = sectionSpec(
|
|
2065
|
+
"## Product Decisions",
|
|
2066
|
+
[
|
|
2067
|
+
"Keep active decisions only, dated inline when added or changed.",
|
|
2068
|
+
"Capture decisions that shape behavior, interfaces, boundaries, compatibility, risk acceptance, or migration constraints.",
|
|
2069
|
+
"Replace stale decisions instead of appending historical logs."
|
|
2070
|
+
],
|
|
2071
|
+
"decision"
|
|
2072
|
+
);
|
|
2073
|
+
var ENGINEERING_DECISIONS_SECTION = sectionSpec(
|
|
2074
|
+
"## Engineering Decisions",
|
|
2075
|
+
[
|
|
2076
|
+
"Keep active engineering, architecture, contract, workflow, or operational decisions only, dated inline when added or changed.",
|
|
2077
|
+
"Do not restate product promises, product rationale, or business decisions here; link product truth instead.",
|
|
2078
|
+
"Replace stale decisions instead of appending historical logs."
|
|
2079
|
+
],
|
|
2080
|
+
"engineering_decisions"
|
|
2081
|
+
);
|
|
2082
|
+
var RATIONALE_SECTION = sectionSpec("## Rationale", [
|
|
2083
|
+
"Explain why the current behavior, structure, or contract is this way, including tradeoffs and constraints.",
|
|
2084
|
+
"Tie rationale to evidence-backed facts and active decisions; do not use this as a changelog."
|
|
2085
|
+
]);
|
|
2086
|
+
var NON_GOALS_SECTION = sectionSpec("## Non-Goals", [
|
|
2087
|
+
"Name adjacent behavior, responsibilities, interfaces, or future expansions this doc intentionally does not own.",
|
|
2088
|
+
"Use this section to prevent scope creep and duplicate truth ownership."
|
|
2089
|
+
]);
|
|
2090
|
+
var MAINTENANCE_NOTES_SECTION = sectionSpec("## Maintenance Notes", [
|
|
2091
|
+
"List related tests, routing cautions, migration notes, compatibility risks, evidence drift risks, and review triggers for future maintainers or agents.",
|
|
2092
|
+
"Keep this operational and current-state focused, not historical."
|
|
2093
|
+
]);
|
|
2094
|
+
var SOURCE_REFERENCES_SECTION = sectionSpec(
|
|
2095
|
+
"## Source References",
|
|
2096
|
+
[
|
|
2097
|
+
"List source files, tests, configs, generated templates, route files, or product instructions that support current claims."
|
|
2098
|
+
],
|
|
2099
|
+
"source_references"
|
|
2100
|
+
);
|
|
2101
|
+
var renderTypedTruthDocTemplate = (truthKind, title, sections) => {
|
|
2213
2102
|
return [
|
|
2214
|
-
"
|
|
2215
|
-
"
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2103
|
+
"---",
|
|
2104
|
+
"status: active",
|
|
2105
|
+
`truth_kind: ${truthKind}`,
|
|
2106
|
+
`last_reviewed: ${currentDate()}`,
|
|
2107
|
+
"---",
|
|
2108
|
+
"",
|
|
2109
|
+
`# ${title}`,
|
|
2110
|
+
"",
|
|
2111
|
+
...renderTemplateSection(PURPOSE_SECTION),
|
|
2112
|
+
...renderTemplateSection(SCOPE_SECTION),
|
|
2113
|
+
...sections.flatMap(renderTemplateSection),
|
|
2114
|
+
...renderTemplateSection(ENGINEERING_DECISIONS_SECTION),
|
|
2115
|
+
...renderTemplateSection(RATIONALE_SECTION),
|
|
2116
|
+
...renderTemplateSection(NON_GOALS_SECTION),
|
|
2117
|
+
...renderTemplateSection(MAINTENANCE_NOTES_SECTION),
|
|
2118
|
+
...renderTemplateSection(SOURCE_REFERENCES_SECTION)
|
|
2220
2119
|
].join("\n");
|
|
2221
2120
|
};
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
var TRUTHMARK_BLOCK_START = "<!-- truthmark:start -->";
|
|
2225
|
-
var TRUTHMARK_BLOCK_END = "<!-- truthmark:end -->";
|
|
2226
|
-
var renderCompactHierarchySummary = (config) => {
|
|
2227
|
-
const productTruthRoot = resolveProductTruthRoot(config);
|
|
2228
|
-
const engineeringTruthRoot = resolveEngineeringTruthRoot(config);
|
|
2229
|
-
const truthDocRoots = Array.from(
|
|
2230
|
-
/* @__PURE__ */ new Set([productTruthRoot, engineeringTruthRoot])
|
|
2231
|
-
).map((truthRoot3) => `${truthRoot3}/**/*.md`);
|
|
2232
|
-
return `Hierarchy hints: config .truthmark/config.yml when present; routes ${config.truthmark.paths.routesIndex} and ${config.truthmark.paths.routeAreasRoot}/**/*.md when present; Truth docs: ${truthDocRoots.join(" and ")} when present.`;
|
|
2233
|
-
};
|
|
2234
|
-
var renderAgentsBlock = (config = defaultAgentConfig()) => {
|
|
2235
|
-
const portalLine = config.truthmark.generated.portal.enabled ? `Truthmark Portal is a separate manual-only presentation workflow. Run it only when explicitly requested; it writes generated non-canonical static files under ${config.truthmark.paths.portalOutput}/. Markdown remains canonical.` : null;
|
|
2121
|
+
var CORE_LANE_INVARIANT = "Do not make product docs a summary of engineering docs. Do not make engineering docs a detailed version of product docs. Product truth says what must be true and why. Engineering truth says how the repository currently realizes it.";
|
|
2122
|
+
var renderProductTruthDocTemplate = (truthKind, title, sections, includeNonGoals) => {
|
|
2236
2123
|
return [
|
|
2237
|
-
|
|
2238
|
-
"
|
|
2124
|
+
"---",
|
|
2125
|
+
"status: active",
|
|
2126
|
+
`truth_kind: ${truthKind}`,
|
|
2127
|
+
`last_reviewed: ${currentDate()}`,
|
|
2128
|
+
"---",
|
|
2239
2129
|
"",
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
"
|
|
2243
|
-
|
|
2244
|
-
"
|
|
2245
|
-
"
|
|
2246
|
-
"
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
...
|
|
2250
|
-
|
|
2251
|
-
|
|
2130
|
+
`# ${title}`,
|
|
2131
|
+
"",
|
|
2132
|
+
"<!--",
|
|
2133
|
+
CORE_LANE_INVARIANT,
|
|
2134
|
+
"Product docs may cite code directly when code proves current product behavior, but keep implementation flow, renderer internals, CLI envelopes, and generated file inventories in engineering truth.",
|
|
2135
|
+
"-->",
|
|
2136
|
+
"",
|
|
2137
|
+
...sections.flatMap(renderTemplateSection),
|
|
2138
|
+
...renderTemplateSection(PRODUCT_DECISIONS_SECTION),
|
|
2139
|
+
...renderTemplateSection(
|
|
2140
|
+
sectionSpec(
|
|
2141
|
+
"## Engineering Realization Links",
|
|
2142
|
+
[
|
|
2143
|
+
"List engineering truth that realizes this product truth; author canonical realized_by links in route YAML, not doc frontmatter.",
|
|
2144
|
+
"Do not summarize those engineering docs."
|
|
2145
|
+
],
|
|
2146
|
+
"engineering_realization_links"
|
|
2147
|
+
)
|
|
2148
|
+
),
|
|
2149
|
+
...includeNonGoals ? renderTemplateSection(NON_GOALS_SECTION) : [],
|
|
2150
|
+
...renderTemplateSection(SOURCE_REFERENCES_SECTION)
|
|
2252
2151
|
].join("\n");
|
|
2253
2152
|
};
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2153
|
+
var renderProductCapabilityDocTemplateFile = () => {
|
|
2154
|
+
return renderProductTruthDocTemplate(
|
|
2155
|
+
"product-capability",
|
|
2156
|
+
"{{title}}",
|
|
2157
|
+
[
|
|
2158
|
+
sectionSpec("## Capability Promise", [
|
|
2159
|
+
"State the single user-visible capability and what must be true for users or stakeholders.",
|
|
2160
|
+
"Do not describe implementation mechanics here."
|
|
2161
|
+
]),
|
|
2162
|
+
sectionSpec("## Users And Value", [
|
|
2163
|
+
"Describe who benefits from the capability and the durable value it protects.",
|
|
2164
|
+
"Tie claims to repository evidence, explicit user instruction, or current behavior."
|
|
2165
|
+
]),
|
|
2166
|
+
sectionSpec("## Capability Scope", [
|
|
2167
|
+
"Define what this capability includes and excludes, including product boundary constraints and adjacent systems.",
|
|
2168
|
+
"Capture important scope limits, ownership boundaries, and non-goal pointers here; keep technical contracts in engineering truth."
|
|
2169
|
+
]),
|
|
2170
|
+
sectionSpec("## Current Product Behavior", [
|
|
2171
|
+
"Describe current implemented user-visible behavior in present tense.",
|
|
2172
|
+
"Code files may appear in Source References when they directly prove current behavior."
|
|
2173
|
+
]),
|
|
2174
|
+
sectionSpec("## Acceptance Criteria", [
|
|
2175
|
+
"List observable criteria that show the capability promise is currently satisfied.",
|
|
2176
|
+
"Include criteria that review whether the capability stays within its stated scope and boundary.",
|
|
2177
|
+
"Use criteria that can be reviewed from repository evidence or explicit product instruction."
|
|
2178
|
+
])
|
|
2179
|
+
],
|
|
2180
|
+
true
|
|
2181
|
+
);
|
|
2267
2182
|
};
|
|
2268
|
-
var
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2183
|
+
var renderContractDocTemplateFile = () => {
|
|
2184
|
+
return renderTypedTruthDocTemplate("engineering-contract", "{{title}}", [
|
|
2185
|
+
sectionSpec("## Contract Surface", [
|
|
2186
|
+
"Identify the owned API, CLI, file format, event, protocol, permission boundary, or integration surface.",
|
|
2187
|
+
"State consumers/producers, stability level, and the source files/tests that define the contract."
|
|
2188
|
+
]),
|
|
2189
|
+
sectionSpec("## Inputs", [
|
|
2190
|
+
"Document accepted parameters, payloads, files, environment/config keys, permissions, and validation rules.",
|
|
2191
|
+
"Include required/optional status, defaults, constraints, and normalization behavior."
|
|
2192
|
+
]),
|
|
2193
|
+
sectionSpec("## Outputs", [
|
|
2194
|
+
"Document returned values, emitted files/events, state changes, side effects, and success diagnostics.",
|
|
2195
|
+
"Make externally observable behavior explicit enough for compatibility review."
|
|
2196
|
+
]),
|
|
2197
|
+
sectionSpec("## Errors And Diagnostics", [
|
|
2198
|
+
"List error classes, exit/status codes, user-facing diagnostics, retries, and recoverability expectations.",
|
|
2199
|
+
"Distinguish validation errors, dependency failures, authorization failures, and internal faults when applicable."
|
|
2200
|
+
]),
|
|
2201
|
+
sectionSpec("## Compatibility Rules", [
|
|
2202
|
+
"State backward/forward compatibility guarantees, tolerated inputs, deprecation rules, and breaking-change triggers.",
|
|
2203
|
+
"Include compatibility tests or review questions that protect the contract."
|
|
2204
|
+
]),
|
|
2205
|
+
sectionSpec("## Versioning And Migration", [
|
|
2206
|
+
"Document version negotiation, schema/API version fields, rollout requirements, migration steps, and rollback expectations.",
|
|
2207
|
+
"State 'Not versioned' only when the implementation truly has no versioning or migration surface."
|
|
2208
|
+
])
|
|
2209
|
+
]);
|
|
2210
|
+
};
|
|
2211
|
+
var renderArchitectureDocTemplateFile = () => {
|
|
2212
|
+
return renderTypedTruthDocTemplate("engineering-architecture", "{{title}}", [
|
|
2213
|
+
sectionSpec("## System Role", [
|
|
2214
|
+
"Describe the current architectural role of this subsystem/component in the larger system.",
|
|
2215
|
+
"State the primary responsibilities, consumers, providers, and why this boundary exists now."
|
|
2216
|
+
]),
|
|
2217
|
+
sectionSpec("## Boundaries", [
|
|
2218
|
+
"Define owned code/config/data, external dependencies, trust boundaries, and interfaces crossed by this architecture.",
|
|
2219
|
+
"Name what is deliberately outside the boundary and link neighboring architecture or contract docs when they own it."
|
|
2220
|
+
]),
|
|
2221
|
+
sectionSpec("## Components", [
|
|
2222
|
+
"List the major runtime/build-time components, modules, services, jobs, or generated artifacts and their responsibilities.",
|
|
2223
|
+
"Keep the component list current and evidence-backed; avoid speculative target architecture."
|
|
2224
|
+
]),
|
|
2225
|
+
sectionSpec("## Data And Control Flow", [
|
|
2226
|
+
"Describe important data movement, command/control paths, synchronization points, state ownership, and failure paths.",
|
|
2227
|
+
"Call out persistence, queues, caches, external calls, and security-sensitive transitions where relevant."
|
|
2228
|
+
]),
|
|
2229
|
+
sectionSpec("## Ownership", [
|
|
2230
|
+
"Document team/module ownership, review responsibility, operational responsibility, and escalation paths if known.",
|
|
2231
|
+
"If ownership is inferred from codeowners, config, or repository structure, cite that evidence."
|
|
2232
|
+
]),
|
|
2233
|
+
sectionSpec("## Cross-Cutting Constraints", [
|
|
2234
|
+
"Record active constraints such as security, privacy, reliability, performance, portability, maintainability, compliance, and cost.",
|
|
2235
|
+
"Tie constraints to source evidence, tests, standards, or operational requirements where available."
|
|
2236
|
+
])
|
|
2237
|
+
]);
|
|
2238
|
+
};
|
|
2239
|
+
var renderWorkflowDocTemplateFile = () => {
|
|
2240
|
+
return renderTypedTruthDocTemplate("engineering-workflow", "{{title}}", [
|
|
2241
|
+
sectionSpec("## Triggers", [
|
|
2242
|
+
"List events, commands, schedules, user actions, webhooks, or dependency signals that start this workflow.",
|
|
2243
|
+
"Include preconditions, authorization requirements, debounce/coalescing behavior, and disabled states when applicable."
|
|
2244
|
+
]),
|
|
2245
|
+
sectionSpec("## Inputs", [
|
|
2246
|
+
"Document data, files, config, context, credentials, and environmental assumptions consumed by the workflow.",
|
|
2247
|
+
"Include validation, defaults, and normalization that happen before execution."
|
|
2248
|
+
]),
|
|
2249
|
+
sectionSpec("## Execution Model", [
|
|
2250
|
+
"Describe synchronous/asynchronous execution, concurrency, locking, leases, batching, ordering, and idempotency behavior.",
|
|
2251
|
+
"State whether the workflow waits for user action, runs in the background, is distributed, or is delegated to another system."
|
|
2252
|
+
]),
|
|
2253
|
+
sectionSpec("## Steps", [
|
|
2254
|
+
"Capture the current ordered steps or phases at a level useful for maintenance and review.",
|
|
2255
|
+
"Reference implementation entrypoints instead of duplicating line-by-line code behavior."
|
|
2256
|
+
]),
|
|
2257
|
+
sectionSpec("## State, Retry, And Failure Behavior", [
|
|
2258
|
+
"Document state transitions, retries, timeouts, compensation, fallback, partial-success, and terminal-failure behavior.",
|
|
2259
|
+
"Make externally visible failure semantics and recovery responsibilities clear."
|
|
2260
|
+
]),
|
|
2261
|
+
sectionSpec("## Outputs", [
|
|
2262
|
+
"List artifacts, state changes, notifications, logs, metrics, diagnostics, and downstream triggers produced by the workflow.",
|
|
2263
|
+
"Include success criteria and handoff points to other truth docs or systems."
|
|
2264
|
+
])
|
|
2265
|
+
]);
|
|
2287
2266
|
};
|
|
2288
|
-
var
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2267
|
+
var renderOperationsDocTemplateFile = () => {
|
|
2268
|
+
return renderTypedTruthDocTemplate("engineering-operations", "{{title}}", [
|
|
2269
|
+
sectionSpec("## Operational Surface", [
|
|
2270
|
+
"Describe what operators, maintainers, or automated systems can observe or control for this surface.",
|
|
2271
|
+
"Include commands, dashboards, alerts, runbooks, jobs, or operational APIs that define current operations."
|
|
2272
|
+
]),
|
|
2273
|
+
sectionSpec("## Runtime Topology", [
|
|
2274
|
+
"Document services, processes, containers, hosts, regions, dependencies, queues, stores, and network boundaries involved at runtime.",
|
|
2275
|
+
"State single-node/local behavior explicitly when there is no distributed topology."
|
|
2276
|
+
]),
|
|
2277
|
+
sectionSpec("## Configuration", [
|
|
2278
|
+
"List operational config, environment variables, feature flags, secrets references, defaults, and reload/restart requirements.",
|
|
2279
|
+
"Do not include secret values; describe storage and rotation expectations instead."
|
|
2280
|
+
]),
|
|
2281
|
+
sectionSpec("## Permissions", [
|
|
2282
|
+
"Document required identities, roles, scopes, filesystem/network permissions, and least-privilege boundaries.",
|
|
2283
|
+
"Include user-facing authorization behavior and operator access requirements when relevant."
|
|
2284
|
+
]),
|
|
2285
|
+
sectionSpec("## Deployment And Rollback", [
|
|
2286
|
+
"Describe deployment mechanism, migration ordering, compatibility windows, rollback path, and known irreversible operations.",
|
|
2287
|
+
"Call out manual review points, smoke checks, and post-deploy verification responsibilities."
|
|
2288
|
+
]),
|
|
2289
|
+
sectionSpec("## Availability And Observability", [
|
|
2290
|
+
"Capture availability expectations, health checks, metrics, logs, traces, alerts, SLO/error-budget signals, and known blind spots.",
|
|
2291
|
+
"Include what maintainers should inspect first during incidents or degraded behavior."
|
|
2292
|
+
])
|
|
2293
|
+
]);
|
|
2294
2294
|
};
|
|
2295
|
-
var
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
${block}
|
|
2324
|
-
|
|
2325
|
-
${after}`;
|
|
2295
|
+
var renderTestBehaviorDocTemplateFile = () => {
|
|
2296
|
+
return renderTypedTruthDocTemplate("engineering-test-behavior", "{{title}}", [
|
|
2297
|
+
sectionSpec("## Test Surface", [
|
|
2298
|
+
"Define the behavior, contract, architecture, or workflow surface these tests verify.",
|
|
2299
|
+
"Link the canonical truth docs and code paths the tests are meant to protect."
|
|
2300
|
+
]),
|
|
2301
|
+
sectionSpec("## Fixtures And Data Model", [
|
|
2302
|
+
"Document fixtures, factories, seeds, mocks/fakes, test repositories, external-service substitutes, and data lifecycle rules.",
|
|
2303
|
+
"Include cleanup, determinism, privacy, and cross-test contamination constraints."
|
|
2304
|
+
]),
|
|
2305
|
+
sectionSpec("## Execution Model", [
|
|
2306
|
+
"Describe how tests run: command, framework, parallelism, isolation, network/filesystem assumptions, and required services.",
|
|
2307
|
+
"State whether tests are unit, integration, e2e, contract, smoke, regression, or generated checks."
|
|
2308
|
+
]),
|
|
2309
|
+
sectionSpec("## Assertions And Invariants", [
|
|
2310
|
+
"List the critical assertions, invariants, failure modes, and negative cases that make the tests meaningful.",
|
|
2311
|
+
"Tie assertions to product/contract rules rather than incidental implementation details."
|
|
2312
|
+
]),
|
|
2313
|
+
sectionSpec("## Isolation Rules", [
|
|
2314
|
+
"Document transaction boundaries, temp directories, fake clocks, network blocking, shared resources, and teardown rules.",
|
|
2315
|
+
"Call out known order dependencies or flake risks and how they are controlled."
|
|
2316
|
+
]),
|
|
2317
|
+
sectionSpec("## Reporting And Failure Semantics", [
|
|
2318
|
+
"Describe diagnostics, snapshots, logs, coverage signals, retry policy, and how maintainers should interpret failures.",
|
|
2319
|
+
"Include escalation or quarantine criteria for flaky or environment-sensitive tests."
|
|
2320
|
+
])
|
|
2321
|
+
]);
|
|
2326
2322
|
};
|
|
2327
2323
|
|
|
2328
2324
|
// src/init/hierarchy.ts
|
|
2329
|
-
import fs5 from "fs/promises";
|
|
2330
2325
|
var truthRoot2 = resolveTruthDocsRoot;
|
|
2331
2326
|
var BEHAVIOR_DOC_TEMPLATE_FILE_NAME = "engineering-behavior.md";
|
|
2332
2327
|
var CONTRACT_DOC_TEMPLATE_FILE_NAME = "engineering-contract.md";
|
|
@@ -2336,7 +2331,7 @@ var OPERATIONS_DOC_TEMPLATE_FILE_NAME = "engineering-operations.md";
|
|
|
2336
2331
|
var TEST_BEHAVIOR_DOC_TEMPLATE_FILE_NAME = "engineering-test-behavior.md";
|
|
2337
2332
|
var PRODUCT_CAPABILITY_DOC_TEMPLATE_FILE_NAME = "product-capability.md";
|
|
2338
2333
|
var rootIndexReferencesChildRoute = async (rootDir, rootIndexPath, childRoutePath) => {
|
|
2339
|
-
const rootIndexSource = await
|
|
2334
|
+
const rootIndexSource = await fs4.readFile(
|
|
2340
2335
|
resolveRepoPath(rootDir, rootIndexPath),
|
|
2341
2336
|
"utf8"
|
|
2342
2337
|
);
|
|
@@ -2357,7 +2352,7 @@ var ensureOrUpdateTruthDocTemplate = async (rootDir, templatePath, defaultTempla
|
|
|
2357
2352
|
if (seededResult.status !== "unchanged") {
|
|
2358
2353
|
return seededResult;
|
|
2359
2354
|
}
|
|
2360
|
-
const existingTemplate = await
|
|
2355
|
+
const existingTemplate = await fs4.readFile(
|
|
2361
2356
|
resolveRepoPath(rootDir, templatePath),
|
|
2362
2357
|
"utf8"
|
|
2363
2358
|
);
|
|
@@ -4489,6 +4484,7 @@ var renderTruthmarkCopilotPortalPrompt = (config = defaultAgentConfig()) => {
|
|
|
4489
4484
|
// src/templates/generated-surfaces.ts
|
|
4490
4485
|
var RETIRED_GENERATED_SURFACES = {
|
|
4491
4486
|
exactPaths: [
|
|
4487
|
+
"CLAUDE.md",
|
|
4492
4488
|
"GEMINI.md",
|
|
4493
4489
|
".github/prompts/truthmark-preview.prompt.md",
|
|
4494
4490
|
".cursor/rules/truthmark-structure.mdc",
|
|
@@ -4659,7 +4655,10 @@ var opencodeFiles = (config, block) => {
|
|
|
4659
4655
|
};
|
|
4660
4656
|
var claudeFiles = (config, block) => {
|
|
4661
4657
|
const files = [
|
|
4662
|
-
|
|
4658
|
+
{
|
|
4659
|
+
path: ".claude/rules/truthmark.md",
|
|
4660
|
+
content: block
|
|
4661
|
+
},
|
|
4663
4662
|
...renderTruthmarkSkillPackage({
|
|
4664
4663
|
skillPath: ".claude/skills/truthmark-structure/SKILL.md",
|
|
4665
4664
|
workflowId: "truthmark-structure",
|
|
@@ -4923,16 +4922,18 @@ var renderGeneratedSurfaceCatalog = (config) => {
|
|
|
4923
4922
|
generated: { portal: { enabled: false } }
|
|
4924
4923
|
}
|
|
4925
4924
|
};
|
|
4926
|
-
const
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
...
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4925
|
+
const baseSurfaces = renderGeneratedSurfaces(baseConfig);
|
|
4926
|
+
const basePaths = new Set(baseSurfaces.map(({ path: path13 }) => path13));
|
|
4927
|
+
for (const surface of [
|
|
4928
|
+
...baseSurfaces,
|
|
4929
|
+
...renderGeneratedSurfaces({
|
|
4930
|
+
...baseConfig,
|
|
4931
|
+
truthmark: {
|
|
4932
|
+
...baseConfig.truthmark,
|
|
4933
|
+
generated: { portal: { enabled: true } }
|
|
4934
|
+
}
|
|
4935
|
+
})
|
|
4936
|
+
]) {
|
|
4936
4937
|
const owner = {
|
|
4937
4938
|
kind: basePaths.has(surface.path) ? "platform" : "portal",
|
|
4938
4939
|
platform
|
|
@@ -4941,7 +4942,10 @@ var renderGeneratedSurfaceCatalog = (config) => {
|
|
|
4941
4942
|
const recognizedContent = surface.content.endsWith("\n") ? surface.content : `${surface.content}
|
|
4942
4943
|
`;
|
|
4943
4944
|
if (existing) {
|
|
4944
|
-
existing.owners.
|
|
4945
|
+
if (!existing.owners.some(
|
|
4946
|
+
(existingOwner) => existingOwner.kind === owner.kind && "platform" in existingOwner && existingOwner.platform === owner.platform
|
|
4947
|
+
))
|
|
4948
|
+
existing.owners.push(owner);
|
|
4945
4949
|
if (!existing.recognizedContents.includes(recognizedContent)) {
|
|
4946
4950
|
existing.recognizedContents.push(recognizedContent);
|
|
4947
4951
|
}
|
|
@@ -4959,6 +4963,7 @@ var renderGeneratedSurfaceCatalog = (config) => {
|
|
|
4959
4963
|
catalog.set(retiredPath, {
|
|
4960
4964
|
path: retiredPath,
|
|
4961
4965
|
content: "",
|
|
4966
|
+
...retiredPath === "CLAUDE.md" ? { managedBlock: true } : {},
|
|
4962
4967
|
owners: [
|
|
4963
4968
|
{
|
|
4964
4969
|
kind: "retired",
|
|
@@ -4974,11 +4979,13 @@ var renderGeneratedSurfaceCatalog = (config) => {
|
|
|
4974
4979
|
};
|
|
4975
4980
|
|
|
4976
4981
|
// src/init/lifecycle.ts
|
|
4977
|
-
import
|
|
4982
|
+
import fs5 from "fs/promises";
|
|
4978
4983
|
var plannedContents = /* @__PURE__ */ new WeakMap();
|
|
4984
|
+
var plannedTargets = /* @__PURE__ */ new WeakMap();
|
|
4985
|
+
var plannedExcludedRoots = /* @__PURE__ */ new WeakMap();
|
|
4979
4986
|
var readFile = async (rootDir, filePath) => {
|
|
4980
4987
|
try {
|
|
4981
|
-
return await
|
|
4988
|
+
return await fs5.readFile(resolveRepoPath(rootDir, filePath), "utf8");
|
|
4982
4989
|
} catch (error) {
|
|
4983
4990
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
4984
4991
|
return null;
|
|
@@ -4991,7 +4998,7 @@ var listFiles = async (rootDir, directory) => {
|
|
|
4991
4998
|
while (stack.length > 0) {
|
|
4992
4999
|
const current = stack.pop();
|
|
4993
5000
|
try {
|
|
4994
|
-
for (const entry of await
|
|
5001
|
+
for (const entry of await fs5.readdir(resolveRepoPath(rootDir, current), {
|
|
4995
5002
|
withFileTypes: true
|
|
4996
5003
|
})) {
|
|
4997
5004
|
const next = `${current}/${entry.name}`;
|
|
@@ -5009,7 +5016,7 @@ var findRetiredSurfaces = async (rootDir) => {
|
|
|
5009
5016
|
const paths = /* @__PURE__ */ new Set();
|
|
5010
5017
|
for (const filePath of RETIRED_GENERATED_SURFACES.exactPaths) {
|
|
5011
5018
|
try {
|
|
5012
|
-
await
|
|
5019
|
+
await fs5.lstat(resolveRepoPath(rootDir, filePath));
|
|
5013
5020
|
paths.add(filePath);
|
|
5014
5021
|
} catch (error) {
|
|
5015
5022
|
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
@@ -5027,7 +5034,7 @@ var findRetiredSurfaces = async (rootDir) => {
|
|
|
5027
5034
|
paths.add(filePath);
|
|
5028
5035
|
let packages = [];
|
|
5029
5036
|
try {
|
|
5030
|
-
packages = (await
|
|
5037
|
+
packages = (await fs5.readdir(resolveRepoPath(rootDir, skillRoot), {
|
|
5031
5038
|
withFileTypes: true
|
|
5032
5039
|
})).filter(
|
|
5033
5040
|
(entry) => entry.isDirectory() && entry.name.startsWith("truthmark-")
|
|
@@ -5040,7 +5047,7 @@ var findRetiredSurfaces = async (rootDir) => {
|
|
|
5040
5047
|
for (const retiredFile of RETIRED_GENERATED_SURFACES.retiredPackageFiles) {
|
|
5041
5048
|
const filePath = `${skillRoot}/${packageName}/${retiredFile}`;
|
|
5042
5049
|
try {
|
|
5043
|
-
await
|
|
5050
|
+
await fs5.lstat(resolveRepoPath(rootDir, filePath));
|
|
5044
5051
|
paths.add(filePath);
|
|
5045
5052
|
} catch (error) {
|
|
5046
5053
|
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
@@ -5050,23 +5057,38 @@ var findRetiredSurfaces = async (rootDir) => {
|
|
|
5050
5057
|
}
|
|
5051
5058
|
return [...paths];
|
|
5052
5059
|
};
|
|
5053
|
-
var buildLifecyclePlan = async (rootDir, config, mode, desired = renderGeneratedSurfaces(config)) => {
|
|
5060
|
+
var buildLifecyclePlan = async (rootDir, config, mode, desired = renderGeneratedSurfaces(config), excludedRoots = []) => {
|
|
5054
5061
|
const desiredPaths = new Set(desired.map(({ path: path13 }) => path13));
|
|
5062
|
+
const desiredTargetPaths = /* @__PURE__ */ new Set();
|
|
5055
5063
|
const entries = [];
|
|
5056
5064
|
const diagnostics = [];
|
|
5065
|
+
const desiredTargets = /* @__PURE__ */ new Map();
|
|
5066
|
+
const targets = /* @__PURE__ */ new Map();
|
|
5057
5067
|
let applicable = true;
|
|
5058
|
-
for (const surface of desired
|
|
5059
|
-
|
|
5068
|
+
for (const surface of desired) {
|
|
5069
|
+
const target = await resolveSafeExactFileTarget(
|
|
5070
|
+
rootDir,
|
|
5071
|
+
surface.path,
|
|
5072
|
+
true,
|
|
5073
|
+
surface.managedBlock === true,
|
|
5074
|
+
excludedRoots
|
|
5075
|
+
);
|
|
5076
|
+
if (!target) {
|
|
5060
5077
|
applicable = false;
|
|
5061
5078
|
entries.push({
|
|
5062
5079
|
path: surface.path,
|
|
5063
5080
|
action: "manual-review",
|
|
5064
|
-
reason: "Managed instruction destination is aliased or not a regular single-link file."
|
|
5081
|
+
reason: surface.managedBlock ? "Managed instruction destination does not resolve to a regular single-link file inside the worktree." : "Generated file destination is aliased or not a regular single-link file."
|
|
5065
5082
|
});
|
|
5083
|
+
} else {
|
|
5084
|
+
desiredTargets.set(surface.path, target);
|
|
5085
|
+
desiredTargetPaths.add(target.path);
|
|
5066
5086
|
}
|
|
5067
5087
|
}
|
|
5068
5088
|
for (const surface of desired.filter(({ managedBlock }) => managedBlock)) {
|
|
5069
|
-
const
|
|
5089
|
+
const target = desiredTargets.get(surface.path);
|
|
5090
|
+
if (!target) continue;
|
|
5091
|
+
const content = await readFile(rootDir, target.path);
|
|
5070
5092
|
if (content !== null && parseManagedBlock(content).status === "malformed") {
|
|
5071
5093
|
applicable = false;
|
|
5072
5094
|
entries.push({
|
|
@@ -5076,6 +5098,7 @@ var buildLifecyclePlan = async (rootDir, config, mode, desired = renderGenerated
|
|
|
5076
5098
|
});
|
|
5077
5099
|
}
|
|
5078
5100
|
}
|
|
5101
|
+
const plannedManagedRemovalTargets = /* @__PURE__ */ new Set();
|
|
5079
5102
|
const catalog = renderGeneratedSurfaceCatalog(config);
|
|
5080
5103
|
for (const retiredPath of await findRetiredSurfaces(rootDir)) {
|
|
5081
5104
|
if (!catalog.some(({ path: catalogPath }) => catalogPath === retiredPath))
|
|
@@ -5093,9 +5116,21 @@ var buildLifecyclePlan = async (rootDir, config, mode, desired = renderGenerated
|
|
|
5093
5116
|
}
|
|
5094
5117
|
for (const surface of catalog) {
|
|
5095
5118
|
if (desiredPaths.has(surface.path)) continue;
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5119
|
+
try {
|
|
5120
|
+
await fs5.lstat(resolveRepoPath(rootDir, surface.path));
|
|
5121
|
+
} catch (error) {
|
|
5122
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
5123
|
+
continue;
|
|
5124
|
+
throw error;
|
|
5125
|
+
}
|
|
5126
|
+
const target = await resolveSafeExactFileTarget(
|
|
5127
|
+
rootDir,
|
|
5128
|
+
surface.path,
|
|
5129
|
+
false,
|
|
5130
|
+
surface.managedBlock === true,
|
|
5131
|
+
excludedRoots
|
|
5132
|
+
);
|
|
5133
|
+
if (!target) {
|
|
5099
5134
|
applicable = false;
|
|
5100
5135
|
entries.push({
|
|
5101
5136
|
path: surface.path,
|
|
@@ -5104,6 +5139,9 @@ var buildLifecyclePlan = async (rootDir, config, mode, desired = renderGenerated
|
|
|
5104
5139
|
});
|
|
5105
5140
|
continue;
|
|
5106
5141
|
}
|
|
5142
|
+
if (desiredTargetPaths.has(target.path)) continue;
|
|
5143
|
+
const content = await readFile(rootDir, target.path);
|
|
5144
|
+
if (content === null) continue;
|
|
5107
5145
|
if (surface.path === "GEMINI.md" || surface.path.startsWith(".gemini/")) {
|
|
5108
5146
|
entries.push({
|
|
5109
5147
|
path: surface.path,
|
|
@@ -5120,11 +5158,14 @@ var buildLifecyclePlan = async (rootDir, config, mode, desired = renderGenerated
|
|
|
5120
5158
|
reason: "Truthmark managed-block markers are malformed."
|
|
5121
5159
|
});
|
|
5122
5160
|
} else if (block.status === "valid") {
|
|
5161
|
+
if (plannedManagedRemovalTargets.has(target.path)) continue;
|
|
5162
|
+
plannedManagedRemovalTargets.add(target.path);
|
|
5123
5163
|
entries.push({
|
|
5124
5164
|
path: surface.path,
|
|
5125
5165
|
action: "remove-managed-block",
|
|
5126
5166
|
reason: "No configured platform owns this managed block."
|
|
5127
5167
|
});
|
|
5168
|
+
targets.set(surface.path, target);
|
|
5128
5169
|
}
|
|
5129
5170
|
} else if (surface.recognizedContents.includes(content)) {
|
|
5130
5171
|
entries.push({
|
|
@@ -5132,6 +5173,7 @@ var buildLifecyclePlan = async (rootDir, config, mode, desired = renderGenerated
|
|
|
5132
5173
|
action: "remove-file",
|
|
5133
5174
|
reason: "Generated file has no active platform owner."
|
|
5134
5175
|
});
|
|
5176
|
+
targets.set(surface.path, target);
|
|
5135
5177
|
} else {
|
|
5136
5178
|
entries.push({
|
|
5137
5179
|
path: surface.path,
|
|
@@ -5166,22 +5208,34 @@ var buildLifecyclePlan = async (rootDir, config, mode, desired = renderGenerated
|
|
|
5166
5208
|
const contents = /* @__PURE__ */ new Map();
|
|
5167
5209
|
for (const entry of entries) {
|
|
5168
5210
|
if (entry.action === "remove-file" || entry.action === "remove-managed-block") {
|
|
5169
|
-
const
|
|
5211
|
+
const target = targets.get(entry.path);
|
|
5212
|
+
const content = target ? await readFile(rootDir, target.path) : null;
|
|
5170
5213
|
if (content !== null) contents.set(entry.path, content);
|
|
5171
5214
|
}
|
|
5172
5215
|
}
|
|
5173
5216
|
plannedContents.set(plan, contents);
|
|
5217
|
+
plannedTargets.set(plan, targets);
|
|
5218
|
+
plannedExcludedRoots.set(plan, excludedRoots);
|
|
5174
5219
|
return plan;
|
|
5175
5220
|
};
|
|
5176
5221
|
var applyLifecyclePlan = async (rootDir, plan) => {
|
|
5177
5222
|
if (!plan.applicable || plan.mode !== "apply") return plan;
|
|
5178
5223
|
const expected = plannedContents.get(plan);
|
|
5224
|
+
const expectedTargets = plannedTargets.get(plan);
|
|
5179
5225
|
const failure = async (entry) => {
|
|
5180
5226
|
if (entry.action !== "remove-file" && entry.action !== "remove-managed-block")
|
|
5181
5227
|
return null;
|
|
5182
|
-
|
|
5228
|
+
const target = await resolveSafeExactFileTarget(
|
|
5229
|
+
rootDir,
|
|
5230
|
+
entry.path,
|
|
5231
|
+
false,
|
|
5232
|
+
entry.action === "remove-managed-block",
|
|
5233
|
+
plannedExcludedRoots.get(plan)
|
|
5234
|
+
);
|
|
5235
|
+
const expectedTarget = expectedTargets?.get(entry.path);
|
|
5236
|
+
if (!target || !expectedTarget || target.path !== expectedTarget.path || target.aliased !== expectedTarget.aliased)
|
|
5183
5237
|
return `Unsafe lifecycle target: ${entry.path}`;
|
|
5184
|
-
const content = await readFile(rootDir,
|
|
5238
|
+
const content = await readFile(rootDir, target.path);
|
|
5185
5239
|
if (content === null || expected?.get(entry.path) !== content)
|
|
5186
5240
|
return `Lifecycle target changed after planning: ${entry.path}`;
|
|
5187
5241
|
if (entry.action === "remove-managed-block" && parseManagedBlock(content).status !== "valid")
|
|
@@ -5207,45 +5261,61 @@ var applyLifecyclePlan = async (rootDir, plan) => {
|
|
|
5207
5261
|
};
|
|
5208
5262
|
}
|
|
5209
5263
|
for (const entry of plan.entries) {
|
|
5210
|
-
const
|
|
5264
|
+
const target = expectedTargets?.get(entry.path);
|
|
5265
|
+
if (!target) continue;
|
|
5266
|
+
const absolutePath = resolveRepoPath(rootDir, target.path);
|
|
5211
5267
|
if (entry.action === "remove-file") {
|
|
5212
|
-
const stat = await
|
|
5268
|
+
const stat = await fs5.lstat(absolutePath);
|
|
5213
5269
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1)
|
|
5214
5270
|
throw new Error(
|
|
5215
5271
|
`Refusing unsafe generated file removal: ${entry.path}`
|
|
5216
5272
|
);
|
|
5217
|
-
await
|
|
5273
|
+
await fs5.rm(absolutePath);
|
|
5218
5274
|
} else if (entry.action === "remove-managed-block") {
|
|
5219
|
-
const stat = await
|
|
5275
|
+
const stat = await fs5.lstat(absolutePath);
|
|
5220
5276
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1)
|
|
5221
5277
|
throw new Error(`Refusing unsafe managed-block removal: ${entry.path}`);
|
|
5222
|
-
const content = await
|
|
5278
|
+
const content = await fs5.readFile(absolutePath, "utf8");
|
|
5223
5279
|
const block = parseManagedBlock(content);
|
|
5224
5280
|
if (block.status !== "valid")
|
|
5225
5281
|
throw new Error(`Managed block changed during removal: ${entry.path}`);
|
|
5226
5282
|
const remaining = `${content.slice(0, block.start)}${content.slice(block.end)}`;
|
|
5227
|
-
if (remaining.trim().length === 0
|
|
5228
|
-
|
|
5283
|
+
if (remaining.trim().length === 0 && !target.aliased)
|
|
5284
|
+
await fs5.rm(absolutePath);
|
|
5285
|
+
else await fs5.writeFile(absolutePath, remaining, "utf8");
|
|
5229
5286
|
}
|
|
5230
5287
|
}
|
|
5231
5288
|
return { ...plan, applied: true };
|
|
5232
5289
|
};
|
|
5233
5290
|
|
|
5234
5291
|
// src/init/init.ts
|
|
5235
|
-
var writeManagedAgentsFile = async (rootDir, path13
|
|
5292
|
+
var writeManagedAgentsFile = async (rootDir, path13, block, excludedRoots) => {
|
|
5293
|
+
const target = await resolveSafeExactFileTarget(
|
|
5294
|
+
rootDir,
|
|
5295
|
+
path13,
|
|
5296
|
+
true,
|
|
5297
|
+
true,
|
|
5298
|
+
excludedRoots
|
|
5299
|
+
);
|
|
5300
|
+
if (!target)
|
|
5301
|
+
throw new Error(`Refusing unsafe managed instruction write: ${path13}`);
|
|
5236
5302
|
let existingContent = null;
|
|
5237
5303
|
try {
|
|
5238
|
-
existingContent = await
|
|
5304
|
+
existingContent = await fs6.readFile(
|
|
5305
|
+
resolveRepoPath(rootDir, target.path),
|
|
5306
|
+
"utf8"
|
|
5307
|
+
);
|
|
5239
5308
|
} catch (error) {
|
|
5240
5309
|
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
5241
5310
|
throw error;
|
|
5242
5311
|
}
|
|
5243
5312
|
}
|
|
5244
|
-
|
|
5313
|
+
const result = await writeRepoFile(
|
|
5245
5314
|
rootDir,
|
|
5246
|
-
|
|
5315
|
+
target.path,
|
|
5247
5316
|
upsertManagedBlock(existingContent, block)
|
|
5248
5317
|
);
|
|
5318
|
+
return { ...result, path: path13 };
|
|
5249
5319
|
};
|
|
5250
5320
|
var diagnosticCategoryForPath = (filePath, config) => {
|
|
5251
5321
|
if (filePath === "AGENTS.md") {
|
|
@@ -5254,7 +5324,7 @@ var diagnosticCategoryForPath = (filePath, config) => {
|
|
|
5254
5324
|
if (filePath === ".github/prompts/truthmark-realize.prompt.md" || filePath.startsWith(".github/skills/truthmark-realize/") || filePath.startsWith(".claude/skills/truthmark-realize/") || filePath.startsWith(".opencode/skills/truthmark-realize/") || filePath.startsWith(".agents/skills/truthmark-realize/") || filePath.startsWith(".antigravity/rules/truthmark-realize") || filePath.startsWith(".cursor/rules/truthmark-realize") || filePath.startsWith(".cursor/skills/truthmark-realize/")) {
|
|
5255
5325
|
return "realization";
|
|
5256
5326
|
}
|
|
5257
|
-
if (filePath === "CLAUDE.md" || filePath === ".github/copilot-instructions.md" || filePath.startsWith(".github/prompts/truthmark-") || filePath.startsWith(".github/agents/truth-") || filePath.startsWith(".github/skills/truthmark-") || filePath.startsWith(".claude/agents/truth-") || filePath.startsWith(".claude/skills/truthmark-") || filePath.startsWith(".opencode/skills/truthmark-") || filePath.startsWith(".opencode/agents/") || filePath.startsWith(".codex/agents/") || filePath.startsWith(".antigravity/rules/truthmark-") || filePath.startsWith(".cursor/rules/truthmark-") || filePath.startsWith(".cursor/skills/truthmark-")) {
|
|
5327
|
+
if (filePath === "CLAUDE.md" || filePath === ".claude/rules/truthmark.md" || filePath === ".github/copilot-instructions.md" || filePath.startsWith(".github/prompts/truthmark-") || filePath.startsWith(".github/agents/truth-") || filePath.startsWith(".github/skills/truthmark-") || filePath.startsWith(".claude/agents/truth-") || filePath.startsWith(".claude/skills/truthmark-") || filePath.startsWith(".opencode/skills/truthmark-") || filePath.startsWith(".opencode/agents/") || filePath.startsWith(".codex/agents/") || filePath.startsWith(".antigravity/rules/truthmark-") || filePath.startsWith(".cursor/rules/truthmark-") || filePath.startsWith(".cursor/skills/truthmark-")) {
|
|
5258
5328
|
return "truth-sync";
|
|
5259
5329
|
}
|
|
5260
5330
|
if (filePath.startsWith(".agents/skills/truthmark-")) {
|
|
@@ -5265,9 +5335,14 @@ var diagnosticCategoryForPath = (filePath, config) => {
|
|
|
5265
5335
|
}
|
|
5266
5336
|
return "config";
|
|
5267
5337
|
};
|
|
5268
|
-
var writePlatformFile = async (rootDir, file) => {
|
|
5338
|
+
var writePlatformFile = async (rootDir, file, excludedRoots) => {
|
|
5269
5339
|
if (file.managedBlock) {
|
|
5270
|
-
return writeManagedAgentsFile(
|
|
5340
|
+
return writeManagedAgentsFile(
|
|
5341
|
+
rootDir,
|
|
5342
|
+
file.path,
|
|
5343
|
+
file.content,
|
|
5344
|
+
excludedRoots
|
|
5345
|
+
);
|
|
5271
5346
|
}
|
|
5272
5347
|
return writeRepoFile(rootDir, file.path, file.content);
|
|
5273
5348
|
};
|
|
@@ -5289,58 +5364,128 @@ var writeDiagnostics = (results, config) => {
|
|
|
5289
5364
|
file: result.path
|
|
5290
5365
|
}));
|
|
5291
5366
|
};
|
|
5292
|
-
var
|
|
5367
|
+
var normalizeRequestedPlatforms = (values) => {
|
|
5368
|
+
const unsupported = values.filter(
|
|
5369
|
+
(value) => !SUPPORTED_PLATFORMS.includes(value)
|
|
5370
|
+
);
|
|
5371
|
+
const selected = new Set(values);
|
|
5372
|
+
return {
|
|
5373
|
+
platforms: SUPPORTED_PLATFORMS.filter((platform) => selected.has(platform)),
|
|
5374
|
+
unsupported: [...new Set(unsupported)]
|
|
5375
|
+
};
|
|
5376
|
+
};
|
|
5377
|
+
var runInit = async (cwd, options = {}) => {
|
|
5293
5378
|
const repository = await getGitRepository(cwd);
|
|
5294
5379
|
const rootDir = repository.worktreePath;
|
|
5380
|
+
const excludedGeneratedRoots = [
|
|
5381
|
+
repository.gitEntryPath,
|
|
5382
|
+
repository.gitDir,
|
|
5383
|
+
repository.gitCommonDir
|
|
5384
|
+
];
|
|
5295
5385
|
const loadedConfig = await loadConfig(rootDir);
|
|
5296
|
-
|
|
5386
|
+
const repositoryData = {
|
|
5387
|
+
repositoryRoot: repository.repositoryRoot,
|
|
5388
|
+
worktreePath: repository.worktreePath,
|
|
5389
|
+
branchName: repository.branchName,
|
|
5390
|
+
isDetached: repository.isDetached,
|
|
5391
|
+
isUnborn: repository.isUnborn
|
|
5392
|
+
};
|
|
5393
|
+
if (loadedConfig.status === "invalid") {
|
|
5297
5394
|
return {
|
|
5298
5395
|
command: "init",
|
|
5299
|
-
summary: "Truthmark init
|
|
5396
|
+
summary: "Truthmark init made no changes because config is invalid.",
|
|
5300
5397
|
diagnostics: loadedConfig.diagnostics,
|
|
5301
|
-
data:
|
|
5302
|
-
repositoryRoot: repository.repositoryRoot,
|
|
5303
|
-
worktreePath: repository.worktreePath,
|
|
5304
|
-
branchName: repository.branchName,
|
|
5305
|
-
isDetached: repository.isDetached,
|
|
5306
|
-
isUnborn: repository.isUnborn
|
|
5307
|
-
}
|
|
5398
|
+
data: repositoryData
|
|
5308
5399
|
};
|
|
5309
5400
|
}
|
|
5401
|
+
const savedPlatforms = loadedConfig.config?.platforms ?? [];
|
|
5402
|
+
let requestedPlatforms;
|
|
5403
|
+
if (options.platforms !== void 0) requestedPlatforms = options.platforms;
|
|
5404
|
+
else if (options.selectPlatforms) {
|
|
5405
|
+
const selected = await options.selectPlatforms(savedPlatforms);
|
|
5406
|
+
if (selected === null)
|
|
5407
|
+
return {
|
|
5408
|
+
command: "init",
|
|
5409
|
+
summary: "Truthmark init cancelled; no repository files were changed.",
|
|
5410
|
+
diagnostics: [],
|
|
5411
|
+
data: { ...repositoryData, cancelled: true }
|
|
5412
|
+
};
|
|
5413
|
+
requestedPlatforms = selected;
|
|
5414
|
+
} else requestedPlatforms = savedPlatforms;
|
|
5415
|
+
const normalized = normalizeRequestedPlatforms(requestedPlatforms);
|
|
5416
|
+
if (normalized.unsupported.length > 0)
|
|
5417
|
+
return {
|
|
5418
|
+
command: "init",
|
|
5419
|
+
summary: "Truthmark init requires supported platform values.",
|
|
5420
|
+
diagnostics: normalized.unsupported.map((platform) => ({
|
|
5421
|
+
category: "config",
|
|
5422
|
+
severity: "error",
|
|
5423
|
+
message: `Unsupported Truthmark platform: ${platform}.`,
|
|
5424
|
+
file: ".truthmark/config.yml"
|
|
5425
|
+
})),
|
|
5426
|
+
data: repositoryData
|
|
5427
|
+
};
|
|
5428
|
+
const config = {
|
|
5429
|
+
...loadedConfig.config ?? createDefaultConfig(),
|
|
5430
|
+
platforms: normalized.platforms
|
|
5431
|
+
};
|
|
5432
|
+
const existingConfigSource = loadedConfig.config ? await fs6.readFile(resolveRepoPath(rootDir, loadedConfig.configPath), "utf8") : null;
|
|
5433
|
+
const configSource = existingConfigSource ? updateConfigPlatforms(existingConfigSource, normalized.platforms) : renderConfig(normalized.platforms);
|
|
5434
|
+
const configDiagnostics = loadedConfig.status === "loaded" ? loadedConfig.diagnostics : [];
|
|
5310
5435
|
const results = [];
|
|
5311
|
-
const config = loadedConfig.config;
|
|
5312
5436
|
const block = renderAgentsBlock(config);
|
|
5313
5437
|
const platformFiles = renderGeneratedSurfaces(config, block);
|
|
5438
|
+
if (!await isSafeExactFile(rootDir, loadedConfig.configPath, true)) {
|
|
5439
|
+
return {
|
|
5440
|
+
command: "init",
|
|
5441
|
+
summary: "Truthmark init made no changes because the config path is unsafe.",
|
|
5442
|
+
diagnostics: [
|
|
5443
|
+
{
|
|
5444
|
+
category: "config",
|
|
5445
|
+
severity: "error",
|
|
5446
|
+
message: "Truthmark config path must be a regular file contained in the repository.",
|
|
5447
|
+
file: loadedConfig.configPath
|
|
5448
|
+
}
|
|
5449
|
+
],
|
|
5450
|
+
data: repositoryData
|
|
5451
|
+
};
|
|
5452
|
+
}
|
|
5314
5453
|
const lifecyclePlan = await buildLifecyclePlan(
|
|
5315
5454
|
rootDir,
|
|
5316
5455
|
config,
|
|
5317
5456
|
"apply",
|
|
5318
|
-
platformFiles
|
|
5457
|
+
platformFiles,
|
|
5458
|
+
excludedGeneratedRoots
|
|
5319
5459
|
);
|
|
5320
5460
|
if (!lifecyclePlan.applicable) {
|
|
5321
5461
|
return {
|
|
5322
5462
|
command: "init",
|
|
5323
5463
|
summary: "Truthmark init made no changes because generated-surface preflight failed.",
|
|
5324
|
-
diagnostics: [...
|
|
5325
|
-
data: { lifecyclePlan }
|
|
5464
|
+
diagnostics: [...configDiagnostics, ...lifecyclePlan.diagnostics],
|
|
5465
|
+
data: { ...repositoryData, lifecyclePlan }
|
|
5326
5466
|
};
|
|
5327
5467
|
}
|
|
5468
|
+
results.push(...await scaffoldHierarchy(rootDir, config));
|
|
5469
|
+
for (const file of platformFiles) {
|
|
5470
|
+
results.push(
|
|
5471
|
+
await writePlatformFile(rootDir, file, excludedGeneratedRoots)
|
|
5472
|
+
);
|
|
5473
|
+
}
|
|
5474
|
+
results.push(
|
|
5475
|
+
await writeRepoFile(rootDir, loadedConfig.configPath, configSource)
|
|
5476
|
+
);
|
|
5328
5477
|
const appliedLifecyclePlan = await applyLifecyclePlan(rootDir, lifecyclePlan);
|
|
5329
5478
|
if (!appliedLifecyclePlan.applicable) {
|
|
5330
5479
|
return {
|
|
5331
5480
|
command: "init",
|
|
5332
5481
|
summary: "Truthmark init made no changes because generated-surface preflight failed.",
|
|
5333
5482
|
diagnostics: [
|
|
5334
|
-
...
|
|
5483
|
+
...configDiagnostics,
|
|
5335
5484
|
...appliedLifecyclePlan.diagnostics
|
|
5336
5485
|
],
|
|
5337
|
-
data: { lifecyclePlan: appliedLifecyclePlan }
|
|
5486
|
+
data: { ...repositoryData, lifecyclePlan: appliedLifecyclePlan }
|
|
5338
5487
|
};
|
|
5339
5488
|
}
|
|
5340
|
-
results.push(...await scaffoldHierarchy(rootDir, config));
|
|
5341
|
-
for (const file of platformFiles) {
|
|
5342
|
-
results.push(await writePlatformFile(rootDir, file));
|
|
5343
|
-
}
|
|
5344
5489
|
const changedResults = results.filter(
|
|
5345
5490
|
(result) => result.status !== "unchanged"
|
|
5346
5491
|
);
|
|
@@ -5351,7 +5496,7 @@ var runInit = async (cwd) => {
|
|
|
5351
5496
|
command: "init",
|
|
5352
5497
|
summary: changedResults.length > 0 || lifecycleChanged ? "Initialized or updated the Truthmark repository scaffold." : "Truthmark repository scaffold is already up to date.",
|
|
5353
5498
|
diagnostics: [
|
|
5354
|
-
...
|
|
5499
|
+
...configDiagnostics,
|
|
5355
5500
|
...appliedLifecyclePlan.diagnostics,
|
|
5356
5501
|
...appliedLifecyclePlan.entries.map((entry) => ({
|
|
5357
5502
|
category: "generated-surface",
|
|
@@ -5362,16 +5507,68 @@ var runInit = async (cwd) => {
|
|
|
5362
5507
|
...writeDiagnostics(results, config)
|
|
5363
5508
|
],
|
|
5364
5509
|
data: {
|
|
5365
|
-
|
|
5366
|
-
worktreePath: repository.worktreePath,
|
|
5367
|
-
branchName: repository.branchName,
|
|
5368
|
-
isDetached: repository.isDetached,
|
|
5369
|
-
isUnborn: repository.isUnborn,
|
|
5510
|
+
...repositoryData,
|
|
5370
5511
|
lifecyclePlan: appliedLifecyclePlan
|
|
5371
5512
|
}
|
|
5372
5513
|
};
|
|
5373
5514
|
};
|
|
5374
5515
|
|
|
5516
|
+
// src/cli/platform-selection.ts
|
|
5517
|
+
import { createInterface } from "readline/promises";
|
|
5518
|
+
var PLATFORM_LABELS = {
|
|
5519
|
+
codex: "Codex",
|
|
5520
|
+
opencode: "OpenCode",
|
|
5521
|
+
"claude-code": "Claude Code",
|
|
5522
|
+
"github-copilot": "GitHub Copilot",
|
|
5523
|
+
antigravity: "Antigravity",
|
|
5524
|
+
cursor: "Cursor"
|
|
5525
|
+
};
|
|
5526
|
+
var normalizePlatforms2 = (platforms) => {
|
|
5527
|
+
const selected = new Set(platforms);
|
|
5528
|
+
return SUPPORTED_PLATFORMS.filter((platform) => selected.has(platform));
|
|
5529
|
+
};
|
|
5530
|
+
var renderPlatformChoices = (defaults) => {
|
|
5531
|
+
const selected = new Set(defaults);
|
|
5532
|
+
return SUPPORTED_PLATFORMS.map(
|
|
5533
|
+
(platform, index) => `${index + 1}. ${PLATFORM_LABELS[platform]} [${platform}]${selected.has(platform) ? " (selected)" : ""}`
|
|
5534
|
+
).join("\n");
|
|
5535
|
+
};
|
|
5536
|
+
var parsePlatformSelection = (input, defaults) => {
|
|
5537
|
+
const value = input.trim().toLowerCase();
|
|
5538
|
+
if (value === "") return normalizePlatforms2(defaults);
|
|
5539
|
+
if (value === "q" || value === "quit") return null;
|
|
5540
|
+
if (value === "none") return [];
|
|
5541
|
+
const selected = value.split(",").map((token) => {
|
|
5542
|
+
const index = Number(token.trim());
|
|
5543
|
+
if (!Number.isInteger(index) || index < 1 || index > SUPPORTED_PLATFORMS.length)
|
|
5544
|
+
throw new Error(`Unsupported platform choice: ${token.trim()}`);
|
|
5545
|
+
return SUPPORTED_PLATFORMS[index - 1];
|
|
5546
|
+
});
|
|
5547
|
+
return normalizePlatforms2(selected);
|
|
5548
|
+
};
|
|
5549
|
+
var promptForPlatforms = async (options) => {
|
|
5550
|
+
options.output.write(`${renderPlatformChoices(options.defaults)}
|
|
5551
|
+
`);
|
|
5552
|
+
const readline = createInterface({ input: options.input, output: options.output });
|
|
5553
|
+
try {
|
|
5554
|
+
for (; ; ) {
|
|
5555
|
+
const answer = await readline.question(
|
|
5556
|
+
"Select platforms by number (comma-separated), 'none' for CLI-only, or 'q' to cancel: "
|
|
5557
|
+
);
|
|
5558
|
+
try {
|
|
5559
|
+
return parsePlatformSelection(answer, options.defaults);
|
|
5560
|
+
} catch (error) {
|
|
5561
|
+
options.output.write(
|
|
5562
|
+
`${error instanceof Error ? error.message : String(error)}
|
|
5563
|
+
`
|
|
5564
|
+
);
|
|
5565
|
+
}
|
|
5566
|
+
}
|
|
5567
|
+
} finally {
|
|
5568
|
+
readline.close();
|
|
5569
|
+
}
|
|
5570
|
+
};
|
|
5571
|
+
|
|
5375
5572
|
// src/init/uninstall.ts
|
|
5376
5573
|
var runUninstall = async (cwd, mode) => {
|
|
5377
5574
|
const repository = await getGitRepository(cwd);
|
|
@@ -5396,7 +5593,8 @@ var runUninstall = async (cwd, mode) => {
|
|
|
5396
5593
|
repository.worktreePath,
|
|
5397
5594
|
loaded.config,
|
|
5398
5595
|
mode,
|
|
5399
|
-
[]
|
|
5596
|
+
[],
|
|
5597
|
+
[repository.gitEntryPath, repository.gitDir, repository.gitCommonDir]
|
|
5400
5598
|
);
|
|
5401
5599
|
const plan = await applyLifecyclePlan(repository.worktreePath, planned);
|
|
5402
5600
|
return {
|
|
@@ -5408,7 +5606,7 @@ var runUninstall = async (cwd, mode) => {
|
|
|
5408
5606
|
};
|
|
5409
5607
|
|
|
5410
5608
|
// src/checks/branch-scope.ts
|
|
5411
|
-
import
|
|
5609
|
+
import fs7 from "fs/promises";
|
|
5412
5610
|
import fg from "fast-glob";
|
|
5413
5611
|
|
|
5414
5612
|
// src/markdown/hash.ts
|
|
@@ -5464,7 +5662,7 @@ var getBranchScopeData = async (cwd) => {
|
|
|
5464
5662
|
}
|
|
5465
5663
|
for (const relativePath of [...relevantFiles].sort()) {
|
|
5466
5664
|
try {
|
|
5467
|
-
const source = await
|
|
5665
|
+
const source = await fs7.readFile(resolveWorktreePath(repository, relativePath), "utf8");
|
|
5468
5666
|
relevantFileHashes[relativePath] = hashText(source);
|
|
5469
5667
|
} catch (error) {
|
|
5470
5668
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
@@ -5481,14 +5679,14 @@ var getBranchScopeData = async (cwd) => {
|
|
|
5481
5679
|
};
|
|
5482
5680
|
|
|
5483
5681
|
// src/checks/authority.ts
|
|
5484
|
-
import
|
|
5682
|
+
import fs8 from "fs/promises";
|
|
5485
5683
|
import fg2 from "fast-glob";
|
|
5486
5684
|
var looksLikeGlob = (pattern) => {
|
|
5487
5685
|
return /[*?[\]{}()!+@]/u.test(pattern);
|
|
5488
5686
|
};
|
|
5489
5687
|
var pathExists = async (absolutePath) => {
|
|
5490
5688
|
try {
|
|
5491
|
-
await
|
|
5689
|
+
await fs8.stat(absolutePath);
|
|
5492
5690
|
return true;
|
|
5493
5691
|
} catch (error) {
|
|
5494
5692
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
@@ -5580,10 +5778,10 @@ var checkAuthority = async (rootDir, config) => {
|
|
|
5580
5778
|
};
|
|
5581
5779
|
|
|
5582
5780
|
// src/checks/frontmatter.ts
|
|
5583
|
-
import
|
|
5781
|
+
import fs9 from "fs/promises";
|
|
5584
5782
|
|
|
5585
5783
|
// src/markdown/frontmatter.ts
|
|
5586
|
-
import { parse as
|
|
5784
|
+
import { parse as parse4 } from "yaml";
|
|
5587
5785
|
var openingDelimiterPattern = /^(?:\uFEFF)?---[ \t]*(?:\r?\n|$)/u;
|
|
5588
5786
|
var closingDelimiterPattern = /^---[ \t]*(?:\r?\n|$)/mu;
|
|
5589
5787
|
var asRecord = (value) => {
|
|
@@ -5614,7 +5812,7 @@ var parseFrontmatter = (source, options = {}) => {
|
|
|
5614
5812
|
let data = {};
|
|
5615
5813
|
if (yamlSource.trim().length > 0) {
|
|
5616
5814
|
try {
|
|
5617
|
-
data = asRecord(
|
|
5815
|
+
data = asRecord(parse4(yamlSource));
|
|
5618
5816
|
} catch (error) {
|
|
5619
5817
|
if (options.throwOnInvalid) {
|
|
5620
5818
|
throw error;
|
|
@@ -5682,7 +5880,7 @@ var checkFrontmatter = async (rootDir, config, markdownPaths, truthDocumentEntri
|
|
|
5682
5880
|
}
|
|
5683
5881
|
const absolutePath = resolveRepoPath(rootDir, markdownPath);
|
|
5684
5882
|
await assertRepoContainment(rootDir, absolutePath);
|
|
5685
|
-
const source = await
|
|
5883
|
+
const source = await fs9.readFile(absolutePath, "utf8");
|
|
5686
5884
|
let document;
|
|
5687
5885
|
try {
|
|
5688
5886
|
parseFrontmatter(source, { throwOnInvalid: true });
|
|
@@ -5772,11 +5970,11 @@ var checkFrontmatter = async (rootDir, config, markdownPaths, truthDocumentEntri
|
|
|
5772
5970
|
};
|
|
5773
5971
|
|
|
5774
5972
|
// src/checks/links.ts
|
|
5775
|
-
import
|
|
5973
|
+
import fs10 from "fs/promises";
|
|
5776
5974
|
import path5 from "path";
|
|
5777
5975
|
var pathExists2 = async (absolutePath) => {
|
|
5778
5976
|
try {
|
|
5779
|
-
await
|
|
5977
|
+
await fs10.stat(absolutePath);
|
|
5780
5978
|
return true;
|
|
5781
5979
|
} catch (error) {
|
|
5782
5980
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
@@ -5792,7 +5990,7 @@ var checkLinks = async (rootDir, markdownPaths) => {
|
|
|
5792
5990
|
continue;
|
|
5793
5991
|
}
|
|
5794
5992
|
const absolutePath = resolveRepoPath(rootDir, markdownPath);
|
|
5795
|
-
const source = await
|
|
5993
|
+
const source = await fs10.readFile(absolutePath, "utf8");
|
|
5796
5994
|
let document;
|
|
5797
5995
|
try {
|
|
5798
5996
|
document = parseMarkdownDocument(source);
|
|
@@ -5834,12 +6032,12 @@ var checkLinks = async (rootDir, markdownPaths) => {
|
|
|
5834
6032
|
};
|
|
5835
6033
|
|
|
5836
6034
|
// src/checks/areas.ts
|
|
5837
|
-
import
|
|
6035
|
+
import fs13 from "fs/promises";
|
|
5838
6036
|
import fg5 from "fast-glob";
|
|
5839
6037
|
import micromatch5 from "micromatch";
|
|
5840
6038
|
|
|
5841
6039
|
// src/git/files.ts
|
|
5842
|
-
import
|
|
6040
|
+
import fs11 from "fs/promises";
|
|
5843
6041
|
import { execa as execa2 } from "execa";
|
|
5844
6042
|
import fg3 from "fast-glob";
|
|
5845
6043
|
import micromatch2 from "micromatch";
|
|
@@ -5876,7 +6074,7 @@ var isCurrentContainedFile = async (rootDir, relativePath) => {
|
|
|
5876
6074
|
try {
|
|
5877
6075
|
const absolutePath = resolveRepoPath(rootDir, relativePath);
|
|
5878
6076
|
await assertRepoContainment(rootDir, absolutePath);
|
|
5879
|
-
return (await
|
|
6077
|
+
return (await fs11.stat(absolutePath)).isFile();
|
|
5880
6078
|
} catch {
|
|
5881
6079
|
return false;
|
|
5882
6080
|
}
|
|
@@ -5901,7 +6099,7 @@ var discoverRepositoryFilePaths = async (rootDir, ignorePatterns) => {
|
|
|
5901
6099
|
};
|
|
5902
6100
|
|
|
5903
6101
|
// src/routing/area-resolver.ts
|
|
5904
|
-
import
|
|
6102
|
+
import fs12 from "fs/promises";
|
|
5905
6103
|
import fg4 from "fast-glob";
|
|
5906
6104
|
import micromatch3 from "micromatch";
|
|
5907
6105
|
var unique = (values) => {
|
|
@@ -5952,7 +6150,7 @@ var ensureChildPath = async (rootDir, areaFilesRoot, filePath) => {
|
|
|
5952
6150
|
var readRouteFile = async (rootDir, filePath) => {
|
|
5953
6151
|
try {
|
|
5954
6152
|
return {
|
|
5955
|
-
source: await
|
|
6153
|
+
source: await fs12.readFile(resolveRepoPath(rootDir, filePath), "utf8"),
|
|
5956
6154
|
diagnostic: null
|
|
5957
6155
|
};
|
|
5958
6156
|
} catch (error) {
|
|
@@ -6264,7 +6462,7 @@ var looksLikeGlob2 = (pattern) => {
|
|
|
6264
6462
|
};
|
|
6265
6463
|
var pathExists3 = async (absolutePath) => {
|
|
6266
6464
|
try {
|
|
6267
|
-
await
|
|
6465
|
+
await fs13.stat(absolutePath);
|
|
6268
6466
|
return true;
|
|
6269
6467
|
} catch (error) {
|
|
6270
6468
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
@@ -6667,7 +6865,7 @@ var checkAreas = async (rootDir, config) => {
|
|
|
6667
6865
|
};
|
|
6668
6866
|
|
|
6669
6867
|
// src/checks/decisions.ts
|
|
6670
|
-
import
|
|
6868
|
+
import fs14 from "fs/promises";
|
|
6671
6869
|
import micromatch6 from "micromatch";
|
|
6672
6870
|
var PRODUCT_CAPABILITY_REQUIRED_HEADINGS = [
|
|
6673
6871
|
"Capability Promise",
|
|
@@ -6783,7 +6981,7 @@ var checkDecisionSections = async (rootDir, config, markdownPaths, truthDocument
|
|
|
6783
6981
|
(filePath) => truthDocumentMap.has(filePath) || isDecisionTruthCandidate(config, filePath)
|
|
6784
6982
|
).sort();
|
|
6785
6983
|
for (const filePath of candidatePaths) {
|
|
6786
|
-
const source = await
|
|
6984
|
+
const source = await fs14.readFile(
|
|
6787
6985
|
resolveRepoPath(rootDir, filePath),
|
|
6788
6986
|
"utf8"
|
|
6789
6987
|
);
|
|
@@ -6825,10 +7023,10 @@ var checkDecisionSections = async (rootDir, config, markdownPaths, truthDocument
|
|
|
6825
7023
|
};
|
|
6826
7024
|
|
|
6827
7025
|
// src/checks/generated-surfaces.ts
|
|
6828
|
-
import
|
|
7026
|
+
import fs15 from "fs/promises";
|
|
6829
7027
|
var readOptionalFile = async (rootDir, filePath) => {
|
|
6830
7028
|
try {
|
|
6831
|
-
return await
|
|
7029
|
+
return await fs15.readFile(resolveRepoPath(rootDir, filePath), "utf8");
|
|
6832
7030
|
} catch (error) {
|
|
6833
7031
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
6834
7032
|
return null;
|
|
@@ -6849,11 +7047,19 @@ var obsoleteGeneratedSurfaceMessage = (surfacePath) => {
|
|
|
6849
7047
|
}
|
|
6850
7048
|
return `Generated surface ${surfacePath} is obsolete; rerun truthmark init.`;
|
|
6851
7049
|
};
|
|
6852
|
-
var checkGeneratedSurfaces = async (rootDir, config) => {
|
|
7050
|
+
var checkGeneratedSurfaces = async (rootDir, config, excludedRoots = []) => {
|
|
6853
7051
|
const diagnostics = [];
|
|
6854
7052
|
const renderedSurfaces = renderGeneratedSurfaces(config);
|
|
6855
7053
|
for (const surface of renderedSurfaces) {
|
|
6856
|
-
const
|
|
7054
|
+
const target = await resolveSafeExactFileTarget(
|
|
7055
|
+
rootDir,
|
|
7056
|
+
surface.path,
|
|
7057
|
+
true,
|
|
7058
|
+
surface.managedBlock === true,
|
|
7059
|
+
excludedRoots
|
|
7060
|
+
);
|
|
7061
|
+
if (!target) continue;
|
|
7062
|
+
const content = await readOptionalFile(rootDir, target.path);
|
|
6857
7063
|
if (content === null) {
|
|
6858
7064
|
diagnostics.push({
|
|
6859
7065
|
category: "generated-surface",
|
|
@@ -6880,13 +7086,14 @@ var checkGeneratedSurfaces = async (rootDir, config) => {
|
|
|
6880
7086
|
rootDir,
|
|
6881
7087
|
config,
|
|
6882
7088
|
"dry-run",
|
|
6883
|
-
renderedSurfaces
|
|
7089
|
+
renderedSurfaces,
|
|
7090
|
+
excludedRoots
|
|
6884
7091
|
);
|
|
6885
7092
|
for (const entry of lifecyclePlan.entries) {
|
|
6886
7093
|
diagnostics.push({
|
|
6887
7094
|
category: "generated-surface",
|
|
6888
7095
|
severity: entry.action === "manual-review" ? "error" : "review",
|
|
6889
|
-
message: entry.action === "preserve" && (entry.path.includes("truthmark-preview") || entry.path.endsWith("helper-manifest.yml") || entry.path.endsWith("support/helper-policy.md") || isRetiredGeminiSurfacePath(entry.path)) ? obsoleteGeneratedSurfaceMessage(entry.path) : entry.action === "preserve" ? `Generated surface ${entry.path} is inactive but was preserved: ${entry.reason}` : `Generated surface ${entry.path} is inactive; rerun truthmark init to reconcile it.`,
|
|
7096
|
+
message: entry.action === "manual-review" ? `Generated surface ${entry.path} requires manual review: ${entry.reason}` : entry.action === "preserve" && (entry.path.includes("truthmark-preview") || entry.path.endsWith("helper-manifest.yml") || entry.path.endsWith("support/helper-policy.md") || isRetiredGeminiSurfacePath(entry.path)) ? obsoleteGeneratedSurfaceMessage(entry.path) : entry.action === "preserve" ? `Generated surface ${entry.path} is inactive but was preserved: ${entry.reason}` : `Generated surface ${entry.path} is inactive; rerun truthmark init to reconcile it.`,
|
|
6890
7097
|
file: entry.path
|
|
6891
7098
|
});
|
|
6892
7099
|
}
|
|
@@ -6898,7 +7105,7 @@ import path11 from "path";
|
|
|
6898
7105
|
import micromatch7 from "micromatch";
|
|
6899
7106
|
|
|
6900
7107
|
// src/repo-index/file-tree.ts
|
|
6901
|
-
import
|
|
7108
|
+
import fs16 from "fs/promises";
|
|
6902
7109
|
import path8 from "path";
|
|
6903
7110
|
|
|
6904
7111
|
// src/truth/source-references.ts
|
|
@@ -7043,7 +7250,7 @@ var discoverRepoFiles = async (rootDir, ignore) => {
|
|
|
7043
7250
|
for (const filePath of discoveredFiles) {
|
|
7044
7251
|
let stat;
|
|
7045
7252
|
try {
|
|
7046
|
-
stat = await
|
|
7253
|
+
stat = await fs16.stat(path8.join(rootDir, filePath));
|
|
7047
7254
|
} catch (error) {
|
|
7048
7255
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
7049
7256
|
continue;
|
|
@@ -7067,7 +7274,7 @@ var discoverRepoFiles = async (rootDir, ignore) => {
|
|
|
7067
7274
|
});
|
|
7068
7275
|
}
|
|
7069
7276
|
if (kind === "doc") {
|
|
7070
|
-
const source = await
|
|
7277
|
+
const source = await fs16.readFile(path8.join(rootDir, filePath), "utf8");
|
|
7071
7278
|
const parsed = parseFrontmatter(source);
|
|
7072
7279
|
const markdown = parseMarkdownDocument(parsed.content);
|
|
7073
7280
|
const title = markdown.headings.find((heading) => heading.depth === 1)?.text ?? null;
|
|
@@ -7095,7 +7302,7 @@ var discoverRepoFiles = async (rootDir, ignore) => {
|
|
|
7095
7302
|
};
|
|
7096
7303
|
|
|
7097
7304
|
// src/repo-index/package-metadata.ts
|
|
7098
|
-
import
|
|
7305
|
+
import fs17 from "fs/promises";
|
|
7099
7306
|
import path9 from "path";
|
|
7100
7307
|
import fg6 from "fast-glob";
|
|
7101
7308
|
var packageManagerFor = async (rootDir, packageDir) => {
|
|
@@ -7108,7 +7315,7 @@ var packageManagerFor = async (rootDir, packageDir) => {
|
|
|
7108
7315
|
];
|
|
7109
7316
|
for (const [lockfile, manager] of lockfiles) {
|
|
7110
7317
|
try {
|
|
7111
|
-
await
|
|
7318
|
+
await fs17.access(path9.join(rootDir, packageDir, lockfile));
|
|
7112
7319
|
return manager;
|
|
7113
7320
|
} catch {
|
|
7114
7321
|
continue;
|
|
@@ -7126,7 +7333,7 @@ var discoverPackageMetadata = async (rootDir) => {
|
|
|
7126
7333
|
const packages = [];
|
|
7127
7334
|
for (const packageFile of packageFiles.sort()) {
|
|
7128
7335
|
const packageDir = path9.posix.dirname(packageFile) === "." ? "" : path9.posix.dirname(packageFile);
|
|
7129
|
-
const raw = JSON.parse(await
|
|
7336
|
+
const raw = JSON.parse(await fs17.readFile(path9.join(rootDir, packageFile), "utf8"));
|
|
7130
7337
|
const scripts = raw.scripts && typeof raw.scripts === "object" ? Object.keys(raw.scripts).sort() : [];
|
|
7131
7338
|
packages.push({
|
|
7132
7339
|
path: packageFile,
|
|
@@ -7231,7 +7438,7 @@ var buildRepoIndex = async (cwd) => {
|
|
|
7231
7438
|
import { execa as execa4 } from "execa";
|
|
7232
7439
|
|
|
7233
7440
|
// src/git/changes.ts
|
|
7234
|
-
import
|
|
7441
|
+
import fs18 from "fs/promises";
|
|
7235
7442
|
import path10 from "path";
|
|
7236
7443
|
import { execa as execa3 } from "execa";
|
|
7237
7444
|
var normalizePath3 = (filePath) => {
|
|
@@ -7243,7 +7450,7 @@ var listChangedPaths = async (cwd, args) => {
|
|
|
7243
7450
|
};
|
|
7244
7451
|
var pathExists4 = async (filePath) => {
|
|
7245
7452
|
try {
|
|
7246
|
-
await
|
|
7453
|
+
await fs18.access(filePath);
|
|
7247
7454
|
return true;
|
|
7248
7455
|
} catch {
|
|
7249
7456
|
return false;
|
|
@@ -7490,13 +7697,13 @@ var checkFreshness = async (rootDir, _config, _truthDocumentPaths, base) => {
|
|
|
7490
7697
|
};
|
|
7491
7698
|
|
|
7492
7699
|
// src/evidence/validate.ts
|
|
7493
|
-
import
|
|
7700
|
+
import fs20 from "fs/promises";
|
|
7494
7701
|
import fg7 from "fast-glob";
|
|
7495
7702
|
|
|
7496
7703
|
// src/evidence/parse.ts
|
|
7497
|
-
import
|
|
7704
|
+
import fs19 from "fs/promises";
|
|
7498
7705
|
import path12 from "path";
|
|
7499
|
-
import { parse as
|
|
7706
|
+
import { parse as parse5 } from "yaml";
|
|
7500
7707
|
var yamlFencePattern = /```ya?ml\s*\n([\s\S]*?)```/giu;
|
|
7501
7708
|
var topLevelEvidenceMarkerPattern = /^evidence\s*:/imu;
|
|
7502
7709
|
var toEvidenceReference = (truthDocPath, raw) => {
|
|
@@ -7514,7 +7721,7 @@ var toEvidenceReference = (truthDocPath, raw) => {
|
|
|
7514
7721
|
};
|
|
7515
7722
|
};
|
|
7516
7723
|
var parseEvidenceReferences = async (rootDir, truthDocPath) => {
|
|
7517
|
-
const source = await
|
|
7724
|
+
const source = await fs19.readFile(path12.join(rootDir, truthDocPath), "utf8");
|
|
7518
7725
|
const parsed = parseFrontmatter(source);
|
|
7519
7726
|
const references = [];
|
|
7520
7727
|
for (const entry of parseSourceReferences(source, truthDocPath)) {
|
|
@@ -7529,7 +7736,7 @@ var parseEvidenceReferences = async (rootDir, truthDocPath) => {
|
|
|
7529
7736
|
if (!topLevelEvidenceMarkerPattern.test(yamlBlock)) {
|
|
7530
7737
|
continue;
|
|
7531
7738
|
}
|
|
7532
|
-
const block =
|
|
7739
|
+
const block = parse5(yamlBlock);
|
|
7533
7740
|
const rawEvidence = block && typeof block === "object" && "evidence" in block ? block.evidence : null;
|
|
7534
7741
|
if (!Array.isArray(rawEvidence)) {
|
|
7535
7742
|
continue;
|
|
@@ -7547,7 +7754,7 @@ var parseEvidenceReferences = async (rootDir, truthDocPath) => {
|
|
|
7547
7754
|
// src/evidence/validate.ts
|
|
7548
7755
|
var pathExists5 = async (filePath) => {
|
|
7549
7756
|
try {
|
|
7550
|
-
await
|
|
7757
|
+
await fs20.access(filePath);
|
|
7551
7758
|
return true;
|
|
7552
7759
|
} catch {
|
|
7553
7760
|
return false;
|
|
@@ -7592,7 +7799,7 @@ var validateHash = async (rootDir, reference) => {
|
|
|
7592
7799
|
if (!reference.contentHash.startsWith("sha256:")) {
|
|
7593
7800
|
return diagnosticFor(reference, `Evidence hash for ${reference.path} must use sha256:.`);
|
|
7594
7801
|
}
|
|
7595
|
-
const source = await
|
|
7802
|
+
const source = await fs20.readFile(resolveRepoPath(rootDir, reference.path), "utf8");
|
|
7596
7803
|
const lines = source.split("\n");
|
|
7597
7804
|
const startLine = reference.startLine ?? 1;
|
|
7598
7805
|
const endLine = reference.endLine ?? lines.length;
|
|
@@ -7606,7 +7813,7 @@ var validateLineSpan = async (rootDir, reference) => {
|
|
|
7606
7813
|
if (reference.startLine === void 0 && reference.endLine === void 0) {
|
|
7607
7814
|
return null;
|
|
7608
7815
|
}
|
|
7609
|
-
const source = await
|
|
7816
|
+
const source = await fs20.readFile(resolveRepoPath(rootDir, reference.path), "utf8");
|
|
7610
7817
|
const lines = source.split("\n");
|
|
7611
7818
|
const startLine = reference.startLine ?? 1;
|
|
7612
7819
|
const endLine = reference.endLine ?? lines.length;
|
|
@@ -7837,7 +8044,11 @@ var runCheck = async (cwd, options = {}) => {
|
|
|
7837
8044
|
markdownPaths,
|
|
7838
8045
|
areas.truthDocumentEntries
|
|
7839
8046
|
);
|
|
7840
|
-
const generatedSurfaces = await checkGeneratedSurfaces(
|
|
8047
|
+
const generatedSurfaces = await checkGeneratedSurfaces(
|
|
8048
|
+
rootDir,
|
|
8049
|
+
loadResult.config,
|
|
8050
|
+
[repository.gitEntryPath, repository.gitDir, repository.gitCommonDir]
|
|
8051
|
+
);
|
|
7841
8052
|
const sourceTraceability = await validateEvidenceReferences(rootDir, areas.truthDocumentPaths);
|
|
7842
8053
|
const freshness = options.base ? await checkFreshness(rootDir, loadResult.config, areas.truthDocumentPaths, options.base) : null;
|
|
7843
8054
|
const diagnostics = [
|
|
@@ -8207,10 +8418,14 @@ var buildWorkflowState = async (cwd, options) => {
|
|
|
8207
8418
|
comparisonBase ? { base: comparisonBase } : {}
|
|
8208
8419
|
);
|
|
8209
8420
|
const diagnostics = [
|
|
8210
|
-
...
|
|
8211
|
-
|
|
8212
|
-
|
|
8213
|
-
|
|
8421
|
+
...new Map(
|
|
8422
|
+
[
|
|
8423
|
+
...loadResult.diagnostics,
|
|
8424
|
+
...repoIndex.diagnostics,
|
|
8425
|
+
...impactSet?.diagnostics ?? [],
|
|
8426
|
+
...checkResult.diagnostics
|
|
8427
|
+
].map((diagnostic) => [JSON.stringify(diagnostic), diagnostic])
|
|
8428
|
+
).values()
|
|
8214
8429
|
];
|
|
8215
8430
|
const applicability = applicabilityFor(
|
|
8216
8431
|
options.workflow,
|
|
@@ -8251,7 +8466,7 @@ var buildWorkflowState = async (cwd, options) => {
|
|
|
8251
8466
|
};
|
|
8252
8467
|
|
|
8253
8468
|
// src/cli/handlers.ts
|
|
8254
|
-
import
|
|
8469
|
+
import fs21 from "fs/promises";
|
|
8255
8470
|
|
|
8256
8471
|
// src/agents/workflow-helper-validation.ts
|
|
8257
8472
|
import { parse as parseYaml2 } from "yaml";
|
|
@@ -8568,11 +8783,16 @@ var validateWriteLeaseText = (leaseText, changedText) => {
|
|
|
8568
8783
|
};
|
|
8569
8784
|
|
|
8570
8785
|
// src/cli/handlers.ts
|
|
8571
|
-
var
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
|
|
8786
|
+
var runInit2 = async (options = {}) => {
|
|
8787
|
+
const interactive = options.platforms === void 0 && !options.json && process.stdin.isTTY && process.stdout.isTTY;
|
|
8788
|
+
return runInit(process.cwd(), {
|
|
8789
|
+
platforms: options.platforms,
|
|
8790
|
+
selectPlatforms: interactive ? (defaults) => promptForPlatforms({
|
|
8791
|
+
defaults,
|
|
8792
|
+
input: process.stdin,
|
|
8793
|
+
output: process.stdout
|
|
8794
|
+
}) : void 0
|
|
8795
|
+
});
|
|
8576
8796
|
};
|
|
8577
8797
|
var runUninstall2 = async (mode) => {
|
|
8578
8798
|
return runUninstall(process.cwd(), mode);
|
|
@@ -8645,9 +8865,22 @@ var invalidWorkflowResult = (command, workflow) => ({
|
|
|
8645
8865
|
request: workflow ? { workflow } : {}
|
|
8646
8866
|
}
|
|
8647
8867
|
});
|
|
8868
|
+
var summarizeValues = (values) => values.length <= 5 ? values.join(", ") : `${values.slice(0, 5).join(", ")} (+${values.length - 5} more)`;
|
|
8869
|
+
var renderWorkflowStatusSummary = (state) => {
|
|
8870
|
+
const card = state.workflowCard;
|
|
8871
|
+
return [
|
|
8872
|
+
`Truthmark workflow status completed for ${state.workflow}.`,
|
|
8873
|
+
`Applicability: ${state.applicability.state}.`,
|
|
8874
|
+
...card.affectedFiles.length > 0 ? [`Affected files: ${summarizeValues(card.affectedFiles)}`] : [],
|
|
8875
|
+
...card.likelyRouteOwners.length > 0 ? [`Likely route owners: ${summarizeValues(card.likelyRouteOwners)}`] : [],
|
|
8876
|
+
...card.suggestedTruthDocs.length > 0 ? [`Suggested truth docs: ${summarizeValues(card.suggestedTruthDocs)}`] : [],
|
|
8877
|
+
...card.openQuestions.length > 0 ? [`Open questions: ${summarizeValues(card.openQuestions)}`] : [],
|
|
8878
|
+
...state.nextSteps.length > 0 ? [`Next steps: ${summarizeValues(state.nextSteps)}`] : []
|
|
8879
|
+
].join("\n");
|
|
8880
|
+
};
|
|
8648
8881
|
var readHelperFile = async (filePath, helper) => {
|
|
8649
8882
|
try {
|
|
8650
|
-
return await
|
|
8883
|
+
return await fs21.readFile(filePath, "utf8");
|
|
8651
8884
|
} catch (error) {
|
|
8652
8885
|
const message = error instanceof Error ? error.message : String(error);
|
|
8653
8886
|
return { ok: false, helper, errors: [`could not read file: ${message}`] };
|
|
@@ -8685,7 +8918,7 @@ var runWorkflowStatus = async (options) => {
|
|
|
8685
8918
|
});
|
|
8686
8919
|
return {
|
|
8687
8920
|
command: "workflow status",
|
|
8688
|
-
summary:
|
|
8921
|
+
summary: renderWorkflowStatusSummary(workflowState),
|
|
8689
8922
|
diagnostics: workflowState.diagnostics,
|
|
8690
8923
|
data: {
|
|
8691
8924
|
request: {
|
|
@@ -8740,27 +8973,36 @@ var writeValidationResult = (command, result, options) => {
|
|
|
8740
8973
|
var addJsonOption = (command) => {
|
|
8741
8974
|
return command.option("--json", "Render command output as JSON");
|
|
8742
8975
|
};
|
|
8976
|
+
var collectPlatform = (value, previous) => [
|
|
8977
|
+
...previous ?? [],
|
|
8978
|
+
value
|
|
8979
|
+
];
|
|
8743
8980
|
var buildProgram = () => {
|
|
8744
8981
|
const program = new Command();
|
|
8745
8982
|
program.name("truthmark").description(
|
|
8746
8983
|
"Git-native, branch-scoped truth workflow installer for local AI coding agents."
|
|
8747
8984
|
).showHelpAfterError();
|
|
8748
|
-
addJsonOption(
|
|
8749
|
-
program.command("config").description(
|
|
8750
|
-
"Create or render the Truthmark repository config before initialization."
|
|
8751
|
-
).option(
|
|
8752
|
-
"--stdout",
|
|
8753
|
-
"Render default config in the JSON data payload without writing"
|
|
8754
|
-
).option("--force", "Overwrite an existing .truthmark/config.yml")
|
|
8755
|
-
).action(async (options) => {
|
|
8756
|
-
writeResult(await runConfig2(options), options);
|
|
8757
|
-
});
|
|
8758
8985
|
addJsonOption(
|
|
8759
8986
|
program.command("init").description(
|
|
8760
8987
|
"Initialize Truthmark workflow files in the current repository."
|
|
8988
|
+
).option(
|
|
8989
|
+
"--platform <id>",
|
|
8990
|
+
"Select a repository agent platform; repeat for multiple platforms",
|
|
8991
|
+
collectPlatform
|
|
8992
|
+
).option(
|
|
8993
|
+
"--clear-platforms",
|
|
8994
|
+
"Remove all configured repository agent platforms"
|
|
8761
8995
|
)
|
|
8762
8996
|
).action(async (options) => {
|
|
8763
|
-
|
|
8997
|
+
if (options.clearPlatforms && options.platform !== void 0) {
|
|
8998
|
+
program.error("truthmark init cannot combine --clear-platforms and --platform");
|
|
8999
|
+
return;
|
|
9000
|
+
}
|
|
9001
|
+
const platforms = options.clearPlatforms ? [] : options.platform;
|
|
9002
|
+
writeResult(
|
|
9003
|
+
await runInit2({ json: options.json, platforms }),
|
|
9004
|
+
options
|
|
9005
|
+
);
|
|
8764
9006
|
});
|
|
8765
9007
|
addJsonOption(
|
|
8766
9008
|
program.command("uninstall").description(
|