dotmd-cli 0.34.0 → 0.35.0
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 +6 -0
- package/package.json +1 -1
- package/src/index.mjs +23 -2
- package/src/validate.mjs +18 -2
package/README.md
CHANGED
|
@@ -757,6 +757,12 @@ export const referenceFields = {
|
|
|
757
757
|
bidirectional: ['related_plans'], // warn if A→B but B↛A
|
|
758
758
|
unidirectional: ['supports_plans'], // one-way, no symmetry check
|
|
759
759
|
};
|
|
760
|
+
// Per-ref opt-out: prefix any value with `>` to mark that specific ref one-way
|
|
761
|
+
// without changing the field's default. Useful for leaf→upstream-parent refs
|
|
762
|
+
// (audits, hub docs) where a back-ref would force editing a stable parent.
|
|
763
|
+
// related_docs:
|
|
764
|
+
// - docs/sibling-design.md # bidirectional (default for the field)
|
|
765
|
+
// - "> docs/audit-beyond-platform.md" # one-way upstream — no back-ref expected
|
|
760
766
|
|
|
761
767
|
export const index = {
|
|
762
768
|
path: 'docs/docs.md',
|
package/package.json
CHANGED
package/src/index.mjs
CHANGED
|
@@ -175,10 +175,30 @@ export function parseDocFile(filePath, config) {
|
|
|
175
175
|
const bodyLinks = extractBodyLinks(body);
|
|
176
176
|
const hasCloseout = /^##\s+Closeout/m.test(body);
|
|
177
177
|
|
|
178
|
-
// Dynamic reference field extraction
|
|
178
|
+
// Dynamic reference field extraction. A leading `>` on a value (e.g.
|
|
179
|
+
// `"> docs/audit-beyond-platform.md"`) marks that single ref as one-way —
|
|
180
|
+
// the prefix is stripped so path resolution still works, and the direction
|
|
181
|
+
// is recorded on a parallel `refFieldDirections[field]` array indexed the
|
|
182
|
+
// same as `refFields[field]`. Bidirectional reciprocity checks consume the
|
|
183
|
+
// directions to skip outbound entries that opted out of expecting a back-ref.
|
|
179
184
|
const refFields = {};
|
|
185
|
+
const refFieldDirections = {};
|
|
180
186
|
for (const field of [...(config.referenceFields.bidirectional || []), ...(config.referenceFields.unidirectional || [])]) {
|
|
181
|
-
|
|
187
|
+
const raw = normalizeStringList(parsedFrontmatter[field]);
|
|
188
|
+
const paths = [];
|
|
189
|
+
const directions = [];
|
|
190
|
+
for (const entry of raw) {
|
|
191
|
+
const oneWay = entry.match(/^>\s*(.+)$/);
|
|
192
|
+
if (oneWay) {
|
|
193
|
+
paths.push(oneWay[1].trim());
|
|
194
|
+
directions.push('one-way');
|
|
195
|
+
} else {
|
|
196
|
+
paths.push(entry);
|
|
197
|
+
directions.push('two-way');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
refFields[field] = paths;
|
|
201
|
+
refFieldDirections[field] = directions;
|
|
182
202
|
}
|
|
183
203
|
|
|
184
204
|
// Tag doc with its root
|
|
@@ -215,6 +235,7 @@ export function parseDocFile(filePath, config) {
|
|
|
215
235
|
checklist,
|
|
216
236
|
bodyLinks,
|
|
217
237
|
refFields,
|
|
238
|
+
refFieldDirections,
|
|
218
239
|
checklistCompletionRate: computeChecklistCompletionRate(checklist),
|
|
219
240
|
hasCloseout,
|
|
220
241
|
hasNextStep: Boolean(nextStep),
|
package/src/validate.mjs
CHANGED
|
@@ -258,26 +258,42 @@ export function checkBidirectionalReferences(docs, config) {
|
|
|
258
258
|
const biFields = config.referenceFields.bidirectional || [];
|
|
259
259
|
if (!biFields.length) return { warnings, errors: [] };
|
|
260
260
|
|
|
261
|
+
// refMap stores `Set<targetPath>` keyed by the source doc path — used to
|
|
262
|
+
// answer "does B reference A?" via .has(). oneWayMap stores the subset of A's
|
|
263
|
+
// outbound refs that opted out of expecting a back-ref (via the `>` prefix in
|
|
264
|
+
// frontmatter, parsed in src/index.mjs:parseDocFile). We split these so the
|
|
265
|
+
// membership check stays cheap (Set.has) while the per-ref directionality
|
|
266
|
+
// filter only consults oneWayMap when iterating outbound edges.
|
|
261
267
|
const refMap = new Map();
|
|
268
|
+
const oneWayMap = new Map();
|
|
262
269
|
for (const doc of docs) {
|
|
263
270
|
const docDir = path.dirname(path.join(config.repoRoot, doc.path));
|
|
264
271
|
const refs = new Set();
|
|
272
|
+
const oneWay = new Set();
|
|
265
273
|
for (const field of biFields) {
|
|
266
|
-
|
|
274
|
+
const entries = doc.refFields[field] || [];
|
|
275
|
+
const dirs = doc.refFieldDirections?.[field] || [];
|
|
276
|
+
for (let i = 0; i < entries.length; i++) {
|
|
277
|
+
const relPath = entries[i];
|
|
267
278
|
// Use the same doc-relative-then-repo-root fallback as validateDoc so
|
|
268
279
|
// both styles produce identical refMap keys; otherwise an entry like
|
|
269
280
|
// `docs/foo.md` (repo-root style) gets keyed as
|
|
270
281
|
// `<doc-parent>/docs/foo.md` and never matches the target's repo path.
|
|
271
282
|
const resolved = resolveRefPath(relPath, docDir, config.repoRoot)
|
|
272
283
|
?? path.resolve(docDir, relPath);
|
|
273
|
-
|
|
284
|
+
const targetPath = toRepoPath(resolved, config.repoRoot);
|
|
285
|
+
refs.add(targetPath);
|
|
286
|
+
if (dirs[i] === 'one-way') oneWay.add(targetPath);
|
|
274
287
|
}
|
|
275
288
|
}
|
|
276
289
|
refMap.set(doc.path, refs);
|
|
290
|
+
oneWayMap.set(doc.path, oneWay);
|
|
277
291
|
}
|
|
278
292
|
|
|
279
293
|
for (const [docPath, refs] of refMap) {
|
|
294
|
+
const oneWay = oneWayMap.get(docPath);
|
|
280
295
|
for (const targetPath of refs) {
|
|
296
|
+
if (oneWay.has(targetPath)) continue;
|
|
281
297
|
const targetRefs = refMap.get(targetPath);
|
|
282
298
|
if (targetRefs && !targetRefs.has(docPath)) {
|
|
283
299
|
warnings.push({ path: docPath, level: 'warning',
|