openvibers/openvibe.contracts Commits
Contracts: docs-currency skips placeholder services with no repository; STATUS.json current (v0.126.0) (#77)
Changes
@@@ -9,7 +9,7 @@"plan": "OpenVibe Development Roadmap (combined), Wave 1; implementation plan rev 3 section 3.1","deployed": false,"deployment": "none of its own: a library and artifact repository published as release tags; services pin a tag and deploy it with their own releases",-- "contracts": "openvibe-contracts v0.106.0 (this package; the newest tag)",++ "contracts": "openvibe-contracts v0.126.0 (this package; the newest tag)","features": ["589 JSON Schemas with valid and invalid fixtures","31 service manifests with lifecycle declarations",@@@ -26,7 +26,20 @@"platform.task@1 (planned, owner network) and ADR-044: one task at OpenVibe.Actor's router with a mode, a per-task and per-day budget, result, cost and explanation, SSE progress, cancel and webhooks","watch.watch@1 and the other Watch contracts (plan T18, planned), three watch events, four capabilities and the placeholder service manifest: user-defined persistent observations over pages, feeds, APIs, events and devices","chat.site-daily-result@1, chat.first-chat-result@1 and chat.sound-result@1: Chat's three new internal reads (kind site-daily for Live's home series, the welcome check, and the sound a !command plays), with chat.stats-request@1 now accepting kind site-daily and the two route lists extended",-- "network.resource.read (active, ADR-048): OpenVibe.Network's resource index, GET /api/v1/resources (common.resource-list-result@1) and GET /api/v1/resources/:ovrn (common.resource-summary@1)"++ "network.resource.read (active, ADR-048): OpenVibe.Network's resource index, GET /api/v1/resources (common.resource-list-result@1) and GET /api/v1/resources/:ovrn (common.resource-summary@1)",++ "OpenVibe.Bot is a site (navigation and the network home list it); bot.robot-profile's mapping.driver gains relay (plan T15 R9)",++ "resource-index capabilities for media, events, host and codes are active (plan T13 step 8) and Host is live; Run and Services register, with partial resource pages, Bot button/point commands and ai.app.run",++ "OpenVibe.Watch runs (alpha, capabilities active) and goes live with its public site (plan T18); OpenVibe.Space and AI's public home launch; Events' public origin is openvibe.events (plan T7); Community's forum capabilities retire",++ "the developer portal moves to OpenVibe.Services and Codes becomes the harness; developer links point at OpenVibe.Services",++ "OpenVibe.Actor becomes a service: the task router, its capabilities and schemas (plan T17, ADR-044)",++ "OpenVibe.Food, OpenVibe.Help and OpenVibe.Work become services (plan T19); OpenVibe.Quest and OpenVibe.Rent become services",++ "the forum returns to Community; Deals goes live, with product copy for the new sites",++ "OpenVibe.MediaHub goes live (openvibe.download as a private drive); services declare the account events they consume (ADR-033, SDK 0.37.0)",++ "ADR-054 — the OpenVibe inventory authority (kinds, definitions, instances, equipped and the ledger), its planned service, capabilities, schemas and events (plan T21 step 1)",++ "OpenVibe.Inventory goes live (inventory.openvibe.network, site position 30) with six capabilities active (ADR-054, plan T21 step 2); a definition may name the services or apps that may grant it",++ "the Workshop v1 (ADR-054 section 6): Workshop kinds, network.badge, staff review (inventory.review-request@1, inventory.definition.review active) and media_id on equipped slots",++ "News, Reviews, Trade, Coupons and OpenRestream go live on their own domains (plan T11); the product at openre.stream is OpenRestream, with OpenVibe Live on by default (live.openre.slot.bind)",++ "ADR-055 (proposed): money and the inventory — primary sales first, gifting next; trading and a market stay closed, and chance is never paid for"],-- "updated": "2026-10-07"++ "updated": "2026-10-10"}@@@ -9,7 +9,9 @@* node scripts/docs-currency.js <root> --only search,live** The services are manifests/services/*.json (their `repository`, e.g. OpenVibers/OpenVibe.Search, is looked up as-- * <root>/OpenVibe.Search). For each checkout:++ * <root>/OpenVibe.Search). A manifest with status "placeholder" is one the roadmap has named but not built, so an++ * absent checkout is skipped (not a gap); a placeholder that does have a checkout is checked like any other. For++ * each checkout:** README.md has a level-2 heading for each of: Purpose, Owns, Does not own, Depends on, Capabilities or Grants,* Acceptance (or Tests), Security, Deploy (or Deployment, Running it in production). "What works" is read from@@@ -60,7 +62,13 @@ function checkRepo(root, manifest) {const name = String(manifest.repository || '').split('/').pop();const dir = path.join(root, name);const gaps = [];-- if (!name || !fs.existsSync(dir)) return { id: manifest.id, repository: manifest.repository, missing: true, gaps: ['no checkout under the root'] };++ if (!name || !fs.existsSync(dir)) {++ // A placeholder service (status "placeholder": named by the roadmap, not built) has no repository to check++ // yet; report it as skipped rather than a gap. A placeholder that does have a checkout falls through and is++ // checked normally.++ if (manifest.status === 'placeholder') return { id: manifest.id, repository: manifest.repository, skipped: 'placeholder', gaps: [] };++ return { id: manifest.id, repository: manifest.repository, missing: true, gaps: ['no checkout under the root'] };++ }const readmePath = path.join(dir, 'README.md');if (!fs.existsSync(readmePath)) gaps.push('README.md missing');else {@@@ -86,6 +94,14 @@ function checkRepo(root, manifest) {return { id: manifest.id, repository: manifest.repository, gaps };}++/** The summary line: skipped placeholders are counted apart from the checkouts that were actually checked. */++function summarize(results) {++ const skipped = results.filter((r) => r.skipped).length;++ const checked = results.length - skipped;++ const bad = results.filter((r) => r.gaps.length).length;++ return `${checked - bad}/${checked} current${skipped ? `, ${skipped} placeholder skipped` : ''}`;++}++function main(argv) {const root = argv.find((a) => !a.startsWith('--'));if (!root) { console.error('usage: docs-currency.js <root with the checkouts> [--json] [--only id,id]'); process.exit(2); }@@@ -97,12 +113,14 @@ function main(argv) {const results = manifests.map((m) => checkRepo(path.resolve(root), m));if (argv.includes('--json')) console.log(JSON.stringify(results, null, 2));else {-- for (const r of results) console.log(`${r.gaps.length ? '✗' : '✓'} ${r.id.padEnd(12)} ${r.gaps.length ? r.gaps.join('; ') : 'current'}`);-- const bad = results.filter((r) => r.gaps.length).length;-- console.log(`${results.length - bad}/${results.length} current`);++ for (const r of results) {++ if (r.skipped) console.log(`– ${r.id.padEnd(12)} placeholder, no repository yet`);++ else console.log(`${r.gaps.length ? '✗' : '✓'} ${r.id.padEnd(12)} ${r.gaps.length ? r.gaps.join('; ') : 'current'}`);++ }++ console.log(summarize(results));}process.exit(results.some((r) => r.gaps.length) ? 1 : 0);}if (require.main === module) main(process.argv.slice(2));--module.exports = { checkRepo, headings, contractsPin, REQUIRED, STATUS_FIELDS };++module.exports = { checkRepo, headings, contractsPin, summarize, REQUIRED, STATUS_FIELDS };nnew file mode 100644@@@ -0,0 +1,53 @@++'use strict';++// Documentation currency (roadmap WS-U task 3): a service manifest with status "placeholder" is one the roadmap++// has named but not built, so an absent repository is not a gap — it is skipped. A placeholder that does have a++// checkout is checked like every other service, and the summary counts skipped services apart from the checked ones.++const assert = require('assert');++const fs = require('fs');++const os = require('os');++const path = require('path');++const { checkRepo, summarize } = require('../scripts/docs-currency.js');++++let checks = 0;++const ok = (cond, msg) => { assert.ok(cond, msg); checks++; };++++const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ovc-docs-currency-'));++const placeholder = { id: 'parked', repository: 'OpenVibers/OpenVibe.Parked', status: 'placeholder' };++try {++ // A placeholder with no checkout is skipped, not failed.++ const skipped = checkRepo(root, placeholder);++ ok(skipped.skipped, 'a placeholder with no checkout is skipped');++ ok(skipped.gaps.length === 0, 'a skipped placeholder is not a gap');++ ok(/placeholder/.test(skipped.skipped), 'the skip says why');++++ // A placeholder that does have a checkout is checked normally: a current checkout is current...++ const dir = path.join(root, 'OpenVibe.Parked');++ fs.mkdirSync(dir);++ fs.writeFileSync(path.join(dir, 'README.md'), [++ '# OpenVibe.Parked', '',++ '## Purpose', 'x', '## Owns', 'x', '## Does not own', 'x', '## Depends on', 'x',++ '## Capabilities', 'x', '## Acceptance', 'x', '## Security', 'x', '## Deploy', 'x', '',++ ].join('\n'));++ fs.writeFileSync(path.join(dir, 'STATUS.json'), JSON.stringify({++ repository: 'OpenVibers/OpenVibe.Parked', stage: 'alpha', deployed: false,++ contracts: 'openvibe-contracts v9.9.9', features: [], updated: '2099-01-01',++ }));++ const current = checkRepo(root, placeholder);++ ok(!current.skipped && current.gaps.length === 0, `a placeholder with a current checkout is current: ${current.gaps.join('; ')}`);++++ // ...and a gap in that checkout is still reported, because it was checked and not skipped.++ fs.rmSync(path.join(dir, 'README.md'));++ const broken = checkRepo(root, placeholder);++ ok(!broken.skipped && broken.gaps.some((g) => /README\.md missing/.test(g)), 'a placeholder with a checkout reports its gaps');++++ // A service with no checkout and no placeholder status is still a gap.++ const missing = checkRepo(root, { id: 'built', repository: 'OpenVibers/OpenVibe.Built', status: 'alpha' });++ ok(!missing.skipped && missing.gaps.some((g) => /no checkout/.test(g)), 'a non-placeholder with no checkout is still a gap');++++ // The summary counts skipped services separately and only the checked ones in the ratio.++ ok(summarize([{ gaps: [] }, { gaps: ['x'] }, { skipped: 'placeholder', gaps: [] }]) === '1/2 current, 1 placeholder skipped', 'the summary counts skipped placeholders separately');++ ok(summarize([{ gaps: [] }, { gaps: [] }]) === '2/2 current', 'without a placeholder the summary is unchanged');++} finally {++ fs.rmSync(root, { recursive: true, force: true });++}++console.log(`docs-currency: ${checks} checks passed`);@@@ -918,6 +918,8 @@ sub(path.join(__dirname, 'zones.test.js'));sub(path.join(__dirname, 'usage-topics.test.js'));sub(path.join(__dirname, 'billing-grace.test.js'));sub(path.join(__dirname, 'resources.test.js'));++// Documentation currency (roadmap WS-U task 3): a placeholder service with no repository yet is skipped, not a gap.++sub(path.join(__dirname, 'docs-currency.test.js'));// ── Estate (plan T1 step 3): docs/ESTATE.md and manifests/repositories are generated ──// A checkout tree is read into one repository manifest and the document; --check fails on drift, and with
@@ -9,7 +9,7 @@
@@ -9,7 +9,7 @@
"plan": "OpenVibe Development Roadmap (combined), Wave 1; implementation plan rev 3 section 3.1",
"plan": "OpenVibe Development Roadmap (combined), Wave 1; implementation plan rev 3 section 3.1",
"deployed": false,
"deployed": false,
"deployment": "none of its own: a library and artifact repository published as release tags; services pin a tag and deploy it with their own releases",
"deployment": "none of its own: a library and artifact repository published as release tags; services pin a tag and deploy it with their own releases",
- "contracts": "openvibe-contracts v0.106.0 (this package; the newest tag)",
+ "contracts": "openvibe-contracts v0.126.0 (this package; the newest tag)",
"features": [
"features": [
"589 JSON Schemas with valid and invalid fixtures",
"589 JSON Schemas with valid and invalid fixtures",
"31 service manifests with lifecycle declarations",
"31 service manifests with lifecycle declarations",
@@ -26,7 +26,20 @@
@@ -26,7 +26,20 @@
"platform.task@1 (planned, owner network) and ADR-044: one task at OpenVibe.Actor's router with a mode, a per-task and per-day budget, result, cost and explanation, SSE progress, cancel and webhooks",
"platform.task@1 (planned, owner network) and ADR-044: one task at OpenVibe.Actor's router with a mode, a per-task and per-day budget, result, cost and explanation, SSE progress, cancel and webhooks",
"watch.watch@1 and the other Watch contracts (plan T18, planned), three watch events, four capabilities and the placeholder service manifest: user-defined persistent observations over pages, feeds, APIs, events and devices",
"watch.watch@1 and the other Watch contracts (plan T18, planned), three watch events, four capabilities and the placeholder service manifest: user-defined persistent observations over pages, feeds, APIs, events and devices",
"chat.site-daily-result@1, chat.first-chat-result@1 and chat.sound-result@1: Chat's three new internal reads (kind site-daily for Live's home series, the welcome check, and the sound a !command plays), with chat.stats-request@1 now accepting kind site-daily and the two route lists extended",
"chat.site-daily-result@1, chat.first-chat-result@1 and chat.sound-result@1: Chat's three new internal reads (kind site-daily for Live's home series, the welcome check, and the sound a !command plays), with chat.stats-request@1 now accepting kind site-daily and the two route lists extended",
- "network.resource.read (active, ADR-048): OpenVibe.Network's resource index, GET /api/v1/resources (common.resource-list-result@1) and GET /api/v1/resources/:ovrn (common.resource-summary@1)"
+ "network.resource.read (active, ADR-048): OpenVibe.Network's resource index, GET /api/v1/resources (common.resource-list-result@1) and GET /api/v1/resources/:ovrn (common.resource-summary@1)",
+ "OpenVibe.Bot is a site (navigation and the network home list it); bot.robot-profile's mapping.driver gains relay (plan T15 R9)",
+ "resource-index capabilities for media, events, host and codes are active (plan T13 step 8) and Host is live; Run and Services register, with partial resource pages, Bot button/point commands and ai.app.run",
+ "OpenVibe.Watch runs (alpha, capabilities active) and goes live with its public site (plan T18); OpenVibe.Space and AI's public home launch; Events' public origin is openvibe.events (plan T7); Community's forum capabilities retire",
+ "the developer portal moves to OpenVibe.Services and Codes becomes the harness; developer links point at OpenVibe.Services",
+ "OpenVibe.Actor becomes a service: the task router, its capabilities and schemas (plan T17, ADR-044)",
+ "OpenVibe.Food, OpenVibe.Help and OpenVibe.Work become services (plan T19); OpenVibe.Quest and OpenVibe.Rent become services",
+ "the forum returns to Community; Deals goes live, with product copy for the new sites",
+ "OpenVibe.MediaHub goes live (openvibe.download as a private drive); services declare the account events they consume (ADR-033, SDK 0.37.0)",
+ "ADR-054 — the OpenVibe inventory authority (kinds, definitions, instances, equipped and the ledger), its planned service, capabilities, schemas and events (plan T21 step 1)",
+ "OpenVibe.Inventory goes live (inventory.openvibe.network, site position 30) with six capabilities active (ADR-054, plan T21 step 2); a definition may name the services or apps that may grant it",
+ "the Workshop v1 (ADR-054 section 6): Workshop kinds, network.badge, staff review (inventory.review-request@1, inventory.definition.review active) and media_id on equipped slots",
+ "News, Reviews, Trade, Coupons and OpenRestream go live on their own domains (plan T11); the product at openre.stream is OpenRestream, with OpenVibe Live on by default (live.openre.slot.bind)",
+ "ADR-055 (proposed): money and the inventory — primary sales first, gifting next; trading and a market stay closed, and chance is never paid for"
],
],
- "updated": "2026-10-07"
+ "updated": "2026-10-10"
}
}
@@ -9,7 +9,9 @@
@@ -9,7 +9,9 @@
* node scripts/docs-currency.js <root> --only search,live
* node scripts/docs-currency.js <root> --only search,live
*
*
* The services are manifests/services/*.json (their `repository`, e.g. OpenVibers/OpenVibe.Search, is looked up as
* The services are manifests/services/*.json (their `repository`, e.g. OpenVibers/OpenVibe.Search, is looked up as
- * <root>/OpenVibe.Search). For each checkout:
+ * <root>/OpenVibe.Search). A manifest with status "placeholder" is one the roadmap has named but not built, so an
+ * absent checkout is skipped (not a gap); a placeholder that does have a checkout is checked like any other. For
+ * each checkout:
*
*
* README.md has a level-2 heading for each of: Purpose, Owns, Does not own, Depends on, Capabilities or Grants,
* README.md has a level-2 heading for each of: Purpose, Owns, Does not own, Depends on, Capabilities or Grants,
* Acceptance (or Tests), Security, Deploy (or Deployment, Running it in production). "What works" is read from
* Acceptance (or Tests), Security, Deploy (or Deployment, Running it in production). "What works" is read from
@@ -60,7 +62,13 @@ function checkRepo(root, manifest) {
@@ -60,7 +62,13 @@ function checkRepo(root, manifest) {
const name = String(manifest.repository || '').split('/').pop();
const name = String(manifest.repository || '').split('/').pop();
const dir = path.join(root, name);
const dir = path.join(root, name);
const gaps = [];
const gaps = [];
- if (!name || !fs.existsSync(dir)) return { id: manifest.id, repository: manifest.repository, missing: true, gaps: ['no checkout under the root'] };
+ if (!name || !fs.existsSync(dir)) {
+ // A placeholder service (status "placeholder": named by the roadmap, not built) has no repository to check
+ // yet; report it as skipped rather than a gap. A placeholder that does have a checkout falls through and is
+ // checked normally.
+ if (manifest.status === 'placeholder') return { id: manifest.id, repository: manifest.repository, skipped: 'placeholder', gaps: [] };
+ return { id: manifest.id, repository: manifest.repository, missing: true, gaps: ['no checkout under the root'] };
+ }
const readmePath = path.join(dir, 'README.md');
const readmePath = path.join(dir, 'README.md');
if (!fs.existsSync(readmePath)) gaps.push('README.md missing');
if (!fs.existsSync(readmePath)) gaps.push('README.md missing');
else {
else {
@@ -86,6 +94,14 @@ function checkRepo(root, manifest) {
@@ -86,6 +94,14 @@ function checkRepo(root, manifest) {
return { id: manifest.id, repository: manifest.repository, gaps };
return { id: manifest.id, repository: manifest.repository, gaps };
}
}
+/** The summary line: skipped placeholders are counted apart from the checkouts that were actually checked. */
+function summarize(results) {
+ const skipped = results.filter((r) => r.skipped).length;
+ const checked = results.length - skipped;
+ const bad = results.filter((r) => r.gaps.length).length;
+ return `${checked - bad}/${checked} current${skipped ? `, ${skipped} placeholder skipped` : ''}`;
+}
+
function main(argv) {
function main(argv) {
const root = argv.find((a) => !a.startsWith('--'));
const root = argv.find((a) => !a.startsWith('--'));
if (!root) { console.error('usage: docs-currency.js <root with the checkouts> [--json] [--only id,id]'); process.exit(2); }
if (!root) { console.error('usage: docs-currency.js <root with the checkouts> [--json] [--only id,id]'); process.exit(2); }
@@ -97,12 +113,14 @@ function main(argv) {
@@ -97,12 +113,14 @@ function main(argv) {
const results = manifests.map((m) => checkRepo(path.resolve(root), m));
const results = manifests.map((m) => checkRepo(path.resolve(root), m));
if (argv.includes('--json')) console.log(JSON.stringify(results, null, 2));
if (argv.includes('--json')) console.log(JSON.stringify(results, null, 2));
else {
else {
- for (const r of results) console.log(`${r.gaps.length ? '✗' : '✓'} ${r.id.padEnd(12)} ${r.gaps.length ? r.gaps.join('; ') : 'current'}`);
- const bad = results.filter((r) => r.gaps.length).length;
- console.log(`${results.length - bad}/${results.length} current`);
+ for (const r of results) {
+ if (r.skipped) console.log(`– ${r.id.padEnd(12)} placeholder, no repository yet`);
+ else console.log(`${r.gaps.length ? '✗' : '✓'} ${r.id.padEnd(12)} ${r.gaps.length ? r.gaps.join('; ') : 'current'}`);
+ }
+ console.log(summarize(results));
}
}
process.exit(results.some((r) => r.gaps.length) ? 1 : 0);
process.exit(results.some((r) => r.gaps.length) ? 1 : 0);
}
}
if (require.main === module) main(process.argv.slice(2));
if (require.main === module) main(process.argv.slice(2));
-module.exports = { checkRepo, headings, contractsPin, REQUIRED, STATUS_FIELDS };
+module.exports = { checkRepo, headings, contractsPin, summarize, REQUIRED, STATUS_FIELDS };
new file mode 100644
new file mode 100644
@@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
+'use strict';
+// Documentation currency (roadmap WS-U task 3): a service manifest with status "placeholder" is one the roadmap
+// has named but not built, so an absent repository is not a gap — it is skipped. A placeholder that does have a
+// checkout is checked like every other service, and the summary counts skipped services apart from the checked ones.
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { checkRepo, summarize } = require('../scripts/docs-currency.js');
+
+let checks = 0;
+const ok = (cond, msg) => { assert.ok(cond, msg); checks++; };
+
+const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ovc-docs-currency-'));
+const placeholder = { id: 'parked', repository: 'OpenVibers/OpenVibe.Parked', status: 'placeholder' };
+try {
+ // A placeholder with no checkout is skipped, not failed.
+ const skipped = checkRepo(root, placeholder);
+ ok(skipped.skipped, 'a placeholder with no checkout is skipped');
+ ok(skipped.gaps.length === 0, 'a skipped placeholder is not a gap');
+ ok(/placeholder/.test(skipped.skipped), 'the skip says why');
+
+ // A placeholder that does have a checkout is checked normally: a current checkout is current...
+ const dir = path.join(root, 'OpenVibe.Parked');
+ fs.mkdirSync(dir);
+ fs.writeFileSync(path.join(dir, 'README.md'), [
+ '# OpenVibe.Parked', '',
+ '## Purpose', 'x', '## Owns', 'x', '## Does not own', 'x', '## Depends on', 'x',
+ '## Capabilities', 'x', '## Acceptance', 'x', '## Security', 'x', '## Deploy', 'x', '',
+ ].join('\n'));
+ fs.writeFileSync(path.join(dir, 'STATUS.json'), JSON.stringify({
+ repository: 'OpenVibers/OpenVibe.Parked', stage: 'alpha', deployed: false,
+ contracts: 'openvibe-contracts v9.9.9', features: [], updated: '2099-01-01',
+ }));
+ const current = checkRepo(root, placeholder);
+ ok(!current.skipped && current.gaps.length === 0, `a placeholder with a current checkout is current: ${current.gaps.join('; ')}`);
+
+ // ...and a gap in that checkout is still reported, because it was checked and not skipped.
+ fs.rmSync(path.join(dir, 'README.md'));
+ const broken = checkRepo(root, placeholder);
+ ok(!broken.skipped && broken.gaps.some((g) => /README\.md missing/.test(g)), 'a placeholder with a checkout reports its gaps');
+
+ // A service with no checkout and no placeholder status is still a gap.
+ const missing = checkRepo(root, { id: 'built', repository: 'OpenVibers/OpenVibe.Built', status: 'alpha' });
+ ok(!missing.skipped && missing.gaps.some((g) => /no checkout/.test(g)), 'a non-placeholder with no checkout is still a gap');
+
+ // The summary counts skipped services separately and only the checked ones in the ratio.
+ ok(summarize([{ gaps: [] }, { gaps: ['x'] }, { skipped: 'placeholder', gaps: [] }]) === '1/2 current, 1 placeholder skipped', 'the summary counts skipped placeholders separately');
+ ok(summarize([{ gaps: [] }, { gaps: [] }]) === '2/2 current', 'without a placeholder the summary is unchanged');
+} finally {
+ fs.rmSync(root, { recursive: true, force: true });
+}
+console.log(`docs-currency: ${checks} checks passed`);
@@ -918,6 +918,8 @@ sub(path.join(__dirname, 'zones.test.js'));
@@ -918,6 +918,8 @@ sub(path.join(__dirname, 'zones.test.js'));
sub(path.join(__dirname, 'usage-topics.test.js'));
sub(path.join(__dirname, 'usage-topics.test.js'));
sub(path.join(__dirname, 'billing-grace.test.js'));
sub(path.join(__dirname, 'billing-grace.test.js'));
sub(path.join(__dirname, 'resources.test.js'));
sub(path.join(__dirname, 'resources.test.js'));
+// Documentation currency (roadmap WS-U task 3): a placeholder service with no repository yet is skipped, not a gap.
+sub(path.join(__dirname, 'docs-currency.test.js'));
// ── Estate (plan T1 step 3): docs/ESTATE.md and manifests/repositories are generated ──
// ── Estate (plan T1 step 3): docs/ESTATE.md and manifests/repositories are generated ──
// A checkout tree is read into one repository manifest and the document; --check fails on drift, and with
// A checkout tree is read into one repository manifest and the document; --check fails on drift, and with