Compare commits
46
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a03d99e31 | ||
|
|
8085982396 | ||
|
|
2c02631324 | ||
|
|
b10cfb1e69 | ||
|
|
c6a2237dde | ||
|
|
f8ac64f58a | ||
|
|
482400d93c | ||
|
|
8ca4564b5e | ||
|
|
a8111998c7 | ||
|
|
269aeda193 | ||
|
|
671b872ec9 | ||
|
|
8079d8dc91 | ||
|
|
caa5b04096 | ||
|
|
49b7943420 | ||
|
|
19e16bd44f | ||
|
|
3bd490c080 | ||
|
|
4b448109dd | ||
|
|
bc1149c15e | ||
|
|
089953a7cf | ||
|
|
7b25be22e9 | ||
|
|
4e3d179e61 | ||
|
|
ae58482b72 | ||
|
|
b2d40dada0 | ||
|
|
d30a4cce00 | ||
|
|
4cd280e48d | ||
|
|
8738a03893 | ||
|
|
04a01be992 | ||
|
|
812e2df1da | ||
|
|
8c292fb32f | ||
|
|
f45928c311 | ||
|
|
4d24ae8618 | ||
|
|
d790572e2e | ||
|
|
d7b1dd9601 | ||
|
|
0db2d19a22 | ||
|
|
8eb7e6354e | ||
|
|
9014a510a9 | ||
|
|
974e4740ab | ||
|
|
9cd6d39b71 | ||
|
|
143f925fd8 | ||
|
|
24294d3b77 | ||
|
|
24caeab057 | ||
|
|
888a6ad29b | ||
|
|
24462f460e | ||
|
|
a480ee83dc | ||
|
|
fd43ed5420 | ||
|
|
1d84bc3f3d |
+8
-1
@@ -1,4 +1,11 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"integration_trunk": "next",
|
||||
"release_branch": "main"
|
||||
"release_branch": "main",
|
||||
"flow": "trunk-release",
|
||||
"canonical_remote": "https://git.mosaicstack.dev/mosaicstack/stack",
|
||||
"canonical_clone": "host:/src/mosaic-stack",
|
||||
"worktree_root": "host:/src/mosaic-stack-worktrees",
|
||||
"worktree_policy": "orchestrator-precreated",
|
||||
"notes": "next=development/integration; main=production release. Never branch work off main. worktree_policy is TRANSITIONAL: the wrapper worktree consumer is BLOCKED on the J3/#1174 amendment (checked roots + capacity guard); pre-creation is the interim orchestration choice, not closed policy — it becomes a timing choice only after the wrapper can validate this root."
|
||||
}
|
||||
|
||||
+49
-4
@@ -38,10 +38,12 @@ when:
|
||||
- event: push
|
||||
branch: main
|
||||
|
||||
# Turbo remote cache (turbo.mosaicstack.dev) is configured via Woodpecker
|
||||
# repository-level environment variables (TURBO_API, TURBO_TEAM, TURBO_TOKEN).
|
||||
# This avoids from_secret which is blocked on pull_request events.
|
||||
# If the env vars aren't set, turbo falls back to local cache only.
|
||||
# Turbo remote cache (turbo.mosaicstack.dev) is wired in publish.yml via the
|
||||
# org-level Woodpecker secret `turbo_token` (events: push/tag/cron/manual/
|
||||
# deployment — never pull_request). This PR pipeline deliberately gets no
|
||||
# remote-cache credentials: an untrusted PR must not be able to write to (or
|
||||
# poison) the shared cache. Without TURBO_* env vars turbo falls back to
|
||||
# local cache only, which is the intended behavior here.
|
||||
|
||||
steps:
|
||||
install:
|
||||
@@ -91,6 +93,15 @@ steps:
|
||||
# and sandboxes a throwaway git repo, so it resolves no real credentials and
|
||||
# joins CI directly rather than the exclusions file.
|
||||
- bash packages/mosaic/framework/tools/git/test-issue-close-fail-closed.sh
|
||||
# Hermetic regression for the git identity ladder (#1356): mock tea on PATH,
|
||||
# sandboxed repo, no real credentials (3/3 green under an empty HOME). Pins
|
||||
# fail-closed: a seat whose login is missing gets a named error, never a
|
||||
# borrowed identity. Joins CI directly; its #1007 exclusion is burned down.
|
||||
- bash packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh
|
||||
# Hermetic regression for issue-view.sh (#1357): mock tea/curl, sandboxed
|
||||
# repo. Pins that comment BODIES render on both paths and that a tea
|
||||
# failure is named as what it was (git-config vs credential).
|
||||
- bash packages/mosaic/framework/tools/git/test-issue-view-comments.sh
|
||||
# Hermetic behavioural regression for the PreToolUse wrapper guard: proves
|
||||
# it still blocks the three mistakes AND still lets reads, unwrapped
|
||||
# endpoints and ordinary commands through. Both directions are asserted —
|
||||
@@ -104,6 +115,40 @@ steps:
|
||||
# stub supplies the scale instead of the host's own checkout.
|
||||
- bash packages/mosaic/framework/tools/git/test-mosaic-worktree-large-repo.sh
|
||||
|
||||
# Canonical repo-structure declaration gate (T51 WP5c, spec §5.4 point 2):
|
||||
# .mosaic/repo.json is the machine-readable structure SSOT consumed by git
|
||||
# wrappers and the T32 gate seat; this is its repo-side CI enforcement.
|
||||
# Path-conditional: runs when the declaration, the vendored validator, or this
|
||||
# pipeline config changes (manual runs always include it). Fails the pipeline
|
||||
# on any VALIDATION_ERROR and enforces the schema_version 2 authoring rule
|
||||
# (--require-v2: edited/new declarations may not stay v1). The validator is
|
||||
# vendored into the framework tree (spec §5.1 final home) — provenance in its
|
||||
# header; the hostile-input suite (101 arms, hermetic) runs alongside so the
|
||||
# gate's own instrument ships in the same commit as the gate.
|
||||
structure-declaration:
|
||||
image: *node_image
|
||||
commands:
|
||||
- apk add --no-cache bash git
|
||||
# MOSAIC_HOST_ROOT is a runtime anchor (spec §1.2a: unset fails closed
|
||||
# for managed validation). CI has no host, so the step provisions an
|
||||
# EXPLICIT fixture root — honest configuration for the resolution path,
|
||||
# never a guess about a real host; the per-host containment checks are
|
||||
# runtime concerns and do not run against a fixture. Grammar, schema,
|
||||
# refs, flow, remote normalization, and path grammar all prove here.
|
||||
- mkdir -p /tmp/t51-ci-hostroot
|
||||
- bash packages/mosaic/framework/tools/structure/validate-repo-json.sh .mosaic/repo.json --require-v2
|
||||
- bash packages/mosaic/framework/tools/structure/test-validate-repo-json.sh
|
||||
environment:
|
||||
MOSAIC_HOST_ROOT: /tmp/t51-ci-hostroot
|
||||
when:
|
||||
- event: pull_request
|
||||
path:
|
||||
include:
|
||||
- '.mosaic/repo.json'
|
||||
- 'packages/mosaic/framework/tools/structure/**'
|
||||
- '.woodpecker/ci.yml'
|
||||
- event: manual
|
||||
|
||||
# Canonical verify:release stage `upgrade-guard`.
|
||||
# Blocking gate (#791): a framework upgrade must never write or delete an
|
||||
# operator-owned path. The HARD GATE proves an unanticipated operator sentinel
|
||||
|
||||
+216
-21
@@ -32,6 +32,11 @@ variables:
|
||||
# non-excluded change still builds, so no transitive dep can silently go stale.
|
||||
# (Woodpecker: `when` entries are OR'd; `path` applies to push/PR only — hence
|
||||
# the separate `event: tag` entry.)
|
||||
# #1407: ONE shared anchor for all three image steps. A second main-only
|
||||
# anchor previously gated build-web/build-appservice, so next-lane pushes
|
||||
# published gateway sha images with no web/appservice counterpart — no
|
||||
# sha-parity set existed for next-lane containerized deploys. Every image
|
||||
# step now builds on next too (sha-only destinations, enforced per step).
|
||||
- &image_build_when
|
||||
- event: tag
|
||||
- event: [push, manual]
|
||||
@@ -44,16 +49,6 @@ variables:
|
||||
- '.woodpecker/**'
|
||||
- event: [push, manual]
|
||||
branch: next
|
||||
- &main_image_build_when
|
||||
- event: tag
|
||||
- event: [push, manual]
|
||||
branch: main
|
||||
path:
|
||||
exclude:
|
||||
- 'packages/mosaic/**'
|
||||
- 'docs/**'
|
||||
- '**/*.md'
|
||||
- '.woodpecker/**'
|
||||
|
||||
when:
|
||||
- branch: [main, next]
|
||||
@@ -73,6 +68,13 @@ steps:
|
||||
# being empty) and on any incomplete verification.
|
||||
verify:
|
||||
image: *node_image
|
||||
environment:
|
||||
# Turbo remote cache (see .woodpecker/ci.yml header comment): org-level
|
||||
# secret, exposed only on trusted events (push/tag/cron/manual/deployment).
|
||||
TURBO_API: https://turbo.mosaicstack.dev
|
||||
TURBO_TEAM: mosaic
|
||||
TURBO_TOKEN:
|
||||
from_secret: turbo_token
|
||||
commands:
|
||||
- *enable_pnpm
|
||||
# (a) Commit identity: the provider's claimed SHA must equal the actual
|
||||
@@ -108,6 +110,13 @@ steps:
|
||||
|
||||
build:
|
||||
image: *node_image
|
||||
environment:
|
||||
# Turbo remote cache (see .woodpecker/ci.yml header comment): org-level
|
||||
# secret, exposed only on trusted events (push/tag/cron/manual/deployment).
|
||||
TURBO_API: https://turbo.mosaicstack.dev
|
||||
TURBO_TEAM: mosaic
|
||||
TURBO_TOKEN:
|
||||
from_secret: turbo_token
|
||||
commands:
|
||||
- *enable_pnpm
|
||||
- pnpm build
|
||||
@@ -202,6 +211,20 @@ steps:
|
||||
echo "@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/" >> ~/.npmrc
|
||||
DIST_TAGS_JSON="$(npm view @mosaicstack/mosaic dist-tags --registry https://git.mosaicstack.dev/api/packages/mosaicstack/npm/ --json)"
|
||||
DIST_TAGS_JSON="$DIST_TAGS_JSON" node -e 'const tags = JSON.parse(process.env.DIST_TAGS_JSON || "{}"); if (!tags || typeof tags !== "object" || !Object.hasOwn(tags, "latest")) { throw new Error("Gitea npm registry did not return a usable dist-tags object"); } console.log("[publish-next] registry dist-tags OK: latest=" + tags.latest);'
|
||||
# #1404: snapshot every publishable manifest BEFORE the transform so the
|
||||
# workspace can be restored byte-exact after publish. The transform
|
||||
# rewrites package.json in place (needed: pnpm publish reads the
|
||||
# workspace manifests); without restore, later steps in this pipeline
|
||||
# (build-gateway kaniko COPY + pnpm install --frozen-lockfile) see
|
||||
# manifests that no longer match pnpm-lock.yaml and fail
|
||||
# ERR_PNPM_OUTDATED_LOCKFILE. Snapshot dir is step-local tmp.
|
||||
SNAPSHOT_DIR="$(mktemp -d /tmp/publish-next-manifests.XXXXXX)"
|
||||
export SNAPSHOT_DIR
|
||||
find apps packages plugins -name package.json -not -path "*/node_modules/*" -not -path "*/dist/*" | while read -r mf; do
|
||||
mkdir -p "$SNAPSHOT_DIR/$(dirname "$mf")"
|
||||
cp -p "$mf" "$SNAPSHOT_DIR/$mf"
|
||||
done
|
||||
echo "[publish-next] snapshotted $(find "$SNAPSHOT_DIR" -name package.json | wc -l) manifests to $SNAPSHOT_DIR"
|
||||
node <<'NODE'
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
@@ -209,23 +232,38 @@ steps:
|
||||
const pipelineNumber = process.env.CI_PIPELINE_NUMBER;
|
||||
const roots = ['apps', 'packages', 'plugins'];
|
||||
const updated = [];
|
||||
const exactVersions = new Map(); // name -> bumped next version
|
||||
|
||||
function walk(dir) {
|
||||
function walk(dir, visit) {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.turbo') continue;
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
const packagePath = path.join(fullPath, 'package.json');
|
||||
if (fs.existsSync(packagePath)) updatePackage(packagePath);
|
||||
walk(fullPath);
|
||||
if (fs.existsSync(packagePath)) {
|
||||
const manifest = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
||||
if (manifest.name?.startsWith('@mosaicstack/') && !manifest.private) {
|
||||
visit(manifest, packagePath);
|
||||
}
|
||||
}
|
||||
walk(fullPath, visit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updatePackage(packagePath) {
|
||||
const manifest = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
||||
if (!manifest.name?.startsWith('@mosaicstack/') || manifest.private) return;
|
||||
// #1389: two passes. Pass 1 bumps every publishable manifest to
|
||||
// <stable+1>-next.<pipeline> exactly as before, recording name ->
|
||||
// bumped version. Pass 2 rewrites every published manifest's
|
||||
// @mosaicstack/* dependency entries (dependencies, devDependencies,
|
||||
// peerDependencies, optionalDependencies) to the EXACT same-pipeline
|
||||
// build. A caret range like ^0.0.3-next.2636 leaves the resolver free
|
||||
// to pick any later build — and on a host with a stale cache, an
|
||||
// installer-side scaffold pinned at stable, or a registry hiccup, that
|
||||
// freedom is how a "next" install ends up executing stable-era code
|
||||
// (web1 evidence: old tier validator, missing migrations). Exact pins
|
||||
// make the defect class unrepresentable regardless of resolver path.
|
||||
function bump(manifest, packagePath) {
|
||||
const stableMatch = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(manifest.version);
|
||||
if (!stableMatch) {
|
||||
throw new Error(manifest.name + " has unsupported semver version '" + manifest.version + "'");
|
||||
@@ -234,13 +272,40 @@ steps:
|
||||
const oldVersion = manifest.version;
|
||||
manifest.version = major + '.' + minor + '.' + (Number(patch) + 1) + '-next.' + pipelineNumber;
|
||||
fs.writeFileSync(packagePath, JSON.stringify(manifest, null, 2) + '\n');
|
||||
exactVersions.set(manifest.name, manifest.version);
|
||||
updated.push(manifest.name + ' ' + oldVersion + ' -> ' + manifest.version);
|
||||
}
|
||||
|
||||
for (const root of roots) walk(root);
|
||||
const DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
||||
let pinnedEntries = 0;
|
||||
function pin(manifest, packagePath) {
|
||||
let changed = false;
|
||||
for (const field of DEP_FIELDS) {
|
||||
const deps = manifest[field];
|
||||
if (!deps || typeof deps !== 'object') continue;
|
||||
for (const [name, range] of Object.entries(deps)) {
|
||||
if (!name.startsWith('@mosaicstack/')) continue;
|
||||
const exact = exactVersions.get(name);
|
||||
if (!exact) {
|
||||
throw new Error(
|
||||
manifest.name + ' depends on ' + name +
|
||||
' which has no bumped version in this publish set — cannot pin');
|
||||
}
|
||||
if (range === exact) continue;
|
||||
deps[name] = exact;
|
||||
pinnedEntries++;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) fs.writeFileSync(packagePath, JSON.stringify(manifest, null, 2) + '\n');
|
||||
}
|
||||
|
||||
for (const root of roots) walk(root, bump);
|
||||
for (const root of roots) walk(root, pin);
|
||||
if (updated.length === 0) throw new Error('No publishable @mosaicstack/* packages found');
|
||||
console.log('[publish-next] computed prerelease versions for ' + updated.length + ' packages:');
|
||||
for (const line of updated) console.log('[publish-next] ' + line);
|
||||
console.log('[publish-next] pinned ' + pinnedEntries + ' @mosaicstack/* dep entries to exact same-pipeline versions across ' + updated.length + ' manifests');
|
||||
NODE
|
||||
pnpm --filter "@mosaicstack/*" --filter "!@mosaicstack/web" --filter "!@mosaicstack/mosaic-as" publish --no-git-checks --access public --tag next
|
||||
EXPECTED_VERSION="$(node -p "require('./packages/mosaic/package.json').version")"
|
||||
@@ -250,6 +315,94 @@ steps:
|
||||
exit 1
|
||||
fi
|
||||
echo "[publish-next] @mosaicstack/mosaic@next resolves to $RESOLVED_VERSION"
|
||||
# #1389 post-publish guard: every freshly published manifest must carry
|
||||
# EXACT same-pipeline @mosaicstack/* dep pins (no ranges, no stable
|
||||
# fallback). A leak here fails the pipeline instead of shipping.
|
||||
node <<'GUARD'
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const pipelineNumber = process.env.CI_PIPELINE_NUMBER;
|
||||
const registry = 'https://git.mosaicstack.dev/api/packages/mosaicstack/npm/';
|
||||
const roots = ['apps', 'packages', 'plugins'];
|
||||
const published = [];
|
||||
function walk(dir) {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.turbo') continue;
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
const packagePath = path.join(fullPath, 'package.json');
|
||||
if (fs.existsSync(packagePath)) {
|
||||
const m = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
||||
if (m.name?.startsWith('@mosaicstack/') && !m.private) published.push(m.name);
|
||||
}
|
||||
walk(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const root of roots) walk(root);
|
||||
let failures = 0;
|
||||
for (const name of published) {
|
||||
let manifest;
|
||||
try {
|
||||
const out = execFileSync('npm', ['view', name + '@next', '--json', '--registry', registry],
|
||||
{ encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
|
||||
const arr = JSON.parse(out);
|
||||
manifest = Array.isArray(arr) ? arr[arr.length - 1] : arr;
|
||||
} catch (e) {
|
||||
console.error('[publish-next-guard] FAIL ' + name + ': npm view failed: ' + e.message);
|
||||
failures++;
|
||||
continue;
|
||||
}
|
||||
const fields = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
||||
for (const field of fields) {
|
||||
const deps = manifest[field];
|
||||
if (!deps || typeof deps !== 'object') continue;
|
||||
for (const [dep, range] of Object.entries(deps)) {
|
||||
if (!dep.startsWith('@mosaicstack/')) continue;
|
||||
const expected = dep === name ? manifest.version : null;
|
||||
const isExactPin = /^\d+\.\d+\.\d+-next\./.test(range);
|
||||
const samePipeline = range.endsWith('-next.' + pipelineNumber);
|
||||
if (!isExactPin) {
|
||||
console.error('[publish-next-guard] FAIL ' + name + ' -> ' + dep + ' range "' + range + '" is not an exact -next pin (stable-leak class, #1389)');
|
||||
failures++;
|
||||
} else if (!samePipeline) {
|
||||
console.error('[publish-next-guard] FAIL ' + name + ' -> ' + dep + ' pinned "' + range + '" but this pipeline published -next.' + pipelineNumber + ' (cross-pipeline pin)');
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failures > 0) {
|
||||
console.error('[publish-next-guard] FATAL: ' + failures + ' dep-pin violation(s) — stable-dep leak into next publish (#1389)');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('[publish-next-guard] OK: all ' + published.length + ' published manifests carry exact same-pipeline @mosaicstack/* dep pins');
|
||||
GUARD
|
||||
# #1404 restore: put the workspace manifests back byte-exact so later
|
||||
# steps (build-gateway frozen-lockfile install) see the committed tree.
|
||||
RESTORE_FAIL=0
|
||||
while read -r mf; do
|
||||
if [ -f "$SNAPSHOT_DIR/$mf" ]; then
|
||||
cp -p "$SNAPSHOT_DIR/$mf" "$mf"
|
||||
else
|
||||
echo "[publish-next] FATAL: no snapshot for $mf — cannot restore (snapshot incomplete?)" >&2
|
||||
RESTORE_FAIL=1
|
||||
fi
|
||||
done < <(find apps packages plugins -name package.json -not -path "*/node_modules/*" -not -path "*/dist/*")
|
||||
# Pristine guard (#1404 red-first control): the publish step must leave
|
||||
# the workspace byte-identical to the checkout for every manifest.
|
||||
# git diff is the arbiter — any residual mutation fails THIS step
|
||||
# instead of surfacing as ERR_PNPM_OUTDATED_LOCKFILE in build-gateway.
|
||||
if ! git diff --exit-code -- '**/package.json' >/dev/null 2>&1; then
|
||||
echo "[publish-next] FATAL: workspace package.json files still differ from HEAD after restore (#1404 class)" >&2
|
||||
git diff --stat -- '**/package.json' >&2 || true
|
||||
RESTORE_FAIL=1
|
||||
fi
|
||||
rm -rf "$SNAPSHOT_DIR"
|
||||
if [ "$RESTORE_FAIL" -ne 0 ]; then exit 1; fi
|
||||
echo "[publish-next] workspace manifests restored byte-exact (git diff clean); later steps see the committed tree"
|
||||
depends_on:
|
||||
- build
|
||||
- verify
|
||||
@@ -305,10 +458,18 @@ steps:
|
||||
depends_on:
|
||||
- build
|
||||
- verify
|
||||
# #1411: publish-next-npm mutates workspace manifests in place during
|
||||
# its transform window and restores them at step end. Any step that
|
||||
# reads the pipeline workspace (kaniko COPY of manifests, later
|
||||
# installs) must run AFTER publish-next-npm, never concurrently —
|
||||
# pipeline 2648 raced a COPY inside the window and failed
|
||||
# ERR_PNPM_OUTDATED_LOCKFILE despite a clean restore. This edge is the
|
||||
# serialization invariant; add it to every new workspace consumer.
|
||||
- publish-next-npm
|
||||
|
||||
build-appservice:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
when: *main_image_build_when
|
||||
when: *image_build_when
|
||||
environment:
|
||||
REGISTRY_USER:
|
||||
from_secret: REGISTRY_USERNAME
|
||||
@@ -322,8 +483,17 @@ steps:
|
||||
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
|
||||
- |
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/stack/appservice:sha-${CI_COMMIT_SHA:0:7}"
|
||||
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
if [ "$CI_COMMIT_BRANCH" = "next" ]; then
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
echo "[publish] FATAL: next appservice publish must be sha-only; refusing tag '$CI_COMMIT_TAG'" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[publish] next appservice publish is sha-only"
|
||||
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/appservice:latest"
|
||||
elif [ -z "$CI_COMMIT_TAG" ]; then
|
||||
echo "[publish] FATAL: appservice image publish may only run for main, next, or tag events" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/appservice:$CI_COMMIT_TAG"
|
||||
@@ -332,10 +502,18 @@ steps:
|
||||
depends_on:
|
||||
- build
|
||||
- verify
|
||||
# #1411: publish-next-npm mutates workspace manifests in place during
|
||||
# its transform window and restores them at step end. Any step that
|
||||
# reads the pipeline workspace (kaniko COPY of manifests, later
|
||||
# installs) must run AFTER publish-next-npm, never concurrently —
|
||||
# pipeline 2648 raced a COPY inside the window and failed
|
||||
# ERR_PNPM_OUTDATED_LOCKFILE despite a clean restore. This edge is the
|
||||
# serialization invariant; add it to every new workspace consumer.
|
||||
- publish-next-npm
|
||||
|
||||
build-web:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
when: *main_image_build_when
|
||||
when: *image_build_when
|
||||
environment:
|
||||
REGISTRY_USER:
|
||||
from_secret: REGISTRY_USERNAME
|
||||
@@ -349,8 +527,17 @@ steps:
|
||||
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
|
||||
- |
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/stack/web:sha-${CI_COMMIT_SHA:0:7}"
|
||||
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
if [ "$CI_COMMIT_BRANCH" = "next" ]; then
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
echo "[publish] FATAL: next web publish must be sha-only; refusing tag '$CI_COMMIT_TAG'" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[publish] next web publish is sha-only"
|
||||
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/web:latest"
|
||||
elif [ -z "$CI_COMMIT_TAG" ]; then
|
||||
echo "[publish] FATAL: web image publish may only run for main, next, or tag events" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/web:$CI_COMMIT_TAG"
|
||||
@@ -359,3 +546,11 @@ steps:
|
||||
depends_on:
|
||||
- build
|
||||
- verify
|
||||
# #1411: publish-next-npm mutates workspace manifests in place during
|
||||
# its transform window and restores them at step end. Any step that
|
||||
# reads the pipeline workspace (kaniko COPY of manifests, later
|
||||
# installs) must run AFTER publish-next-npm, never concurrently —
|
||||
# pipeline 2648 raced a COPY inside the window and failed
|
||||
# ERR_PNPM_OUTDATED_LOCKFILE despite a clean restore. This edge is the
|
||||
# serialization invariant; add it to every new workspace consumer.
|
||||
- publish-next-npm
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
// The module under test imports @mosaicstack/db at module scope; we replace only the
|
||||
// pieces DatabaseModule uses (partial mock — the real module also exports the
|
||||
// schema the storage adapter's import chain needs) so the test pins the #1392
|
||||
// contract (refuse to start on an incomplete schema) without a live database.
|
||||
vi.mock('@mosaicstack/db', async (importOriginal) => {
|
||||
const actual: object = await importOriginal();
|
||||
return {
|
||||
...actual,
|
||||
createDb: vi.fn(),
|
||||
createPgliteDb: vi.fn(),
|
||||
getMigrationStatus: vi.fn(),
|
||||
runPgliteMigrations: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
import { DatabaseModule } from './database.module.js';
|
||||
import { getMigrationStatus } from '@mosaicstack/db';
|
||||
import type { DbHandle } from '@mosaicstack/db';
|
||||
import type { StorageAdapter } from '@mosaicstack/storage';
|
||||
import type { MosaicConfig } from '@mosaicstack/config';
|
||||
|
||||
function makeModule(storageType: 'postgres' | 'pglite', tier: string) {
|
||||
const storageAdapter = {
|
||||
name: storageType,
|
||||
migrate: vi.fn(),
|
||||
close: vi.fn(),
|
||||
} as unknown as StorageAdapter;
|
||||
const handle = { close: vi.fn() } as unknown as DbHandle;
|
||||
const config = {
|
||||
tier,
|
||||
storage: { type: storageType, url: 'postgresql://x' },
|
||||
} as unknown as MosaicConfig;
|
||||
return {
|
||||
mod: new DatabaseModule(handle, storageAdapter, config),
|
||||
storageAdapter,
|
||||
};
|
||||
}
|
||||
|
||||
describe('DatabaseModule.onModuleInit — #1392 schema verification', () => {
|
||||
it('refuses to start when the postgres schema is incomplete', async () => {
|
||||
const { mod, storageAdapter } = makeModule('postgres', 'standalone');
|
||||
vi.mocked(getMigrationStatus).mockResolvedValue({
|
||||
appliedCount: 15,
|
||||
expectedCount: 17,
|
||||
expectedLastTag: '0016_salty_morlocks',
|
||||
complete: false,
|
||||
});
|
||||
await expect(mod.onModuleInit()).rejects.toThrow('Database schema incomplete: 15/17');
|
||||
expect(storageAdapter.migrate).toHaveBeenCalled(); // migrations attempted first
|
||||
});
|
||||
|
||||
it('starts normally when the schema is complete', async () => {
|
||||
const { mod } = makeModule('postgres', 'standalone');
|
||||
vi.mocked(getMigrationStatus).mockResolvedValue({
|
||||
appliedCount: 17,
|
||||
expectedCount: 17,
|
||||
expectedLastTag: '0016_salty_morlocks',
|
||||
complete: true,
|
||||
});
|
||||
await expect(mod.onModuleInit()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not verify postgres status for the local tier (PGlite migrates itself)', async () => {
|
||||
const { mod } = makeModule('pglite', 'local');
|
||||
vi.mocked(getMigrationStatus).mockClear();
|
||||
await expect(mod.onModuleInit()).resolves.toBeUndefined();
|
||||
expect(getMigrationStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import {
|
||||
createDb,
|
||||
createPgliteDb,
|
||||
getMigrationStatus,
|
||||
runPgliteMigrations,
|
||||
type Db,
|
||||
type DbHandle,
|
||||
@@ -74,6 +75,11 @@ export class DatabaseModule implements OnApplicationShutdown, OnModuleInit {
|
||||
// the same DATABASE_URL, so a single call covers both the gateway DB and
|
||||
// the storage tables. We deliberately do NOT call runMigrations() here to
|
||||
// avoid opening a second short-lived connection and doubling startup cost.
|
||||
//
|
||||
// #1392: we DO verify afterwards (getMigrationStatus opens one short-lived
|
||||
// connection) and refuse to start on an incomplete schema. A gateway that
|
||||
// boots "healthy" on an empty or partial database is precisely the failure
|
||||
// that shipped in the T63 batch: silent at startup, catastrophic later.
|
||||
async onModuleInit(): Promise<void> {
|
||||
if (this.config.tier === 'local') {
|
||||
this.logger.log('Applying PGlite schema migrations...');
|
||||
@@ -81,6 +87,24 @@ export class DatabaseModule implements OnApplicationShutdown, OnModuleInit {
|
||||
}
|
||||
this.logger.log(`Initializing storage adapter (${this.storageAdapter.name})...`);
|
||||
await this.storageAdapter.migrate();
|
||||
|
||||
if (this.config.storage.type === 'postgres') {
|
||||
const status = await getMigrationStatus(this.config.storage.url);
|
||||
if (!status.complete) {
|
||||
this.logger.error(
|
||||
`Database schema incomplete: ${status.appliedCount.toString()}/${status.expectedCount.toString()} migrations applied ` +
|
||||
`(last expected: ${status.expectedLastTag}). ` +
|
||||
'Refusing to start on a partial schema — see issues #1392/#1402. ' +
|
||||
"Remediation: re-run 'mosaic gateway install' (it now verifies), or apply migrations manually.",
|
||||
);
|
||||
throw new Error(
|
||||
`Database schema incomplete: ${status.appliedCount.toString()}/${status.expectedCount.toString()} migrations applied`,
|
||||
);
|
||||
}
|
||||
this.logger.log(
|
||||
`Database schema verified: ${status.appliedCount.toString()}/${status.expectedCount.toString()} migrations applied.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async onApplicationShutdown(): Promise<void> {
|
||||
|
||||
@@ -14,10 +14,16 @@ import { mountMcpHandler } from './mcp/mcp.controller.js';
|
||||
import { McpService } from './mcp/mcp.service.js';
|
||||
import { detectAndAssertTier, TierDetectionError } from '@mosaicstack/storage';
|
||||
import { resolveGatewayConfigPath } from './env.js';
|
||||
import { assertValidationPipeSeesDtoDecorators } from './validation-pipe-check.js';
|
||||
|
||||
async function bootstrap(): Promise<void> {
|
||||
const logger = new Logger('Bootstrap');
|
||||
|
||||
// Fail loud BEFORE anything else if the global ValidationPipe cannot see
|
||||
// the guarded DTOs' decorated properties (#1391): a broken metatype turns
|
||||
// every request body into a 400 at first use; this surfaces it at boot.
|
||||
assertValidationPipeSeesDtoDecorators();
|
||||
|
||||
if (!process.env['BETTER_AUTH_SECRET']) {
|
||||
throw new Error('BETTER_AUTH_SECRET is required');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Boot-time ValidationPipe metatype self-check (#1391).
|
||||
*
|
||||
* The check exists to fail loud at boot when the global pipe cannot see a
|
||||
* guarded DTO's decorated properties — the #436 class-erasure signature and
|
||||
* its dependency-graph cousins. Red/green arms:
|
||||
*
|
||||
* GREEN real module state: BootstrapSetupDto's three properties are
|
||||
* decorated and visible through the globalThis-shared storage.
|
||||
* RED a control class with NO decorators (the erasure shape): the
|
||||
* check throws PipeMetatypeCheckError naming every property.
|
||||
* RED-2 a control where one property is decorated and two are not: the
|
||||
* error names exactly the missing two — the miss list is precise,
|
||||
* not a blanket failure.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { IsString } from 'class-validator';
|
||||
import {
|
||||
assertValidationPipeSeesDtoDecorators,
|
||||
PipeMetatypeCheckError,
|
||||
} from './validation-pipe-check.js';
|
||||
|
||||
describe('assertValidationPipeSeesDtoDecorators (#1391 boot check)', () => {
|
||||
it('GREEN: passes on real module state (decorated DTO visible to the pipe)', () => {
|
||||
expect(() => assertValidationPipeSeesDtoDecorators()).not.toThrow();
|
||||
});
|
||||
|
||||
it('RED control: a class whose properties lost their decorators throws, naming them', async () => {
|
||||
// Simulate metatype erasure: an undecorated class standing where a
|
||||
// decorated DTO should be. Redefine the guard table for the test by
|
||||
// importing the module and pointing its table at the eroded class —
|
||||
// the check reads the table at call time, so a fresh module instance
|
||||
// with a swapped table reproduces the boot failure deterministically.
|
||||
const { PIPE_GUARDED_DTOS } = await import('./validation-pipe-check.js');
|
||||
|
||||
class ErodedDto {
|
||||
name?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
const original = PIPE_GUARDED_DTOS[0];
|
||||
expect(original).toBeDefined();
|
||||
// Swap in the eroded target (same declared properties, zero decorators).
|
||||
(
|
||||
PIPE_GUARDED_DTOS as unknown as Array<{ name: string; target: object; properties: string[] }>
|
||||
).splice(0, PIPE_GUARDED_DTOS.length, {
|
||||
name: 'ErodedDto',
|
||||
target: ErodedDto,
|
||||
properties: ['name', 'email', 'password'],
|
||||
});
|
||||
|
||||
try {
|
||||
expect(() => assertValidationPipeSeesDtoDecorators()).toThrow(PipeMetatypeCheckError);
|
||||
try {
|
||||
assertValidationPipeSeesDtoDecorators();
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : '';
|
||||
expect(message).toContain('ErodedDto.name');
|
||||
expect(message).toContain('ErodedDto.email');
|
||||
expect(message).toContain('ErodedDto.password');
|
||||
}
|
||||
} finally {
|
||||
// Restore real module state for any later test in this file.
|
||||
(PIPE_GUARDED_DTOS as unknown as unknown[]).splice(0, PIPE_GUARDED_DTOS.length, original);
|
||||
}
|
||||
// And confirm the restore is real.
|
||||
expect(() => assertValidationPipeSeesDtoDecorators()).not.toThrow();
|
||||
});
|
||||
|
||||
it('RED-2 control: a partially decorated class names exactly the missing properties', async () => {
|
||||
const { PIPE_GUARDED_DTOS } = await import('./validation-pipe-check.js');
|
||||
|
||||
class HalfErodedDto {
|
||||
@IsString()
|
||||
name?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
const original = PIPE_GUARDED_DTOS[0];
|
||||
(
|
||||
PIPE_GUARDED_DTOS as unknown as Array<{ name: string; target: object; properties: string[] }>
|
||||
).splice(0, PIPE_GUARDED_DTOS.length, {
|
||||
name: 'HalfErodedDto',
|
||||
target: HalfErodedDto,
|
||||
properties: ['name', 'email', 'password'],
|
||||
});
|
||||
|
||||
try {
|
||||
try {
|
||||
assertValidationPipeSeesDtoDecorators();
|
||||
expect.unreachable('partially decorated DTO must fail the boot check');
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : '';
|
||||
expect(message).toContain('HalfErodedDto.email');
|
||||
expect(message).toContain('HalfErodedDto.password');
|
||||
expect(message).not.toContain('HalfErodedDto.name has no');
|
||||
}
|
||||
} finally {
|
||||
(PIPE_GUARDED_DTOS as unknown as unknown[]).splice(0, PIPE_GUARDED_DTOS.length, original);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'reflect-metadata';
|
||||
import { getMetadataStorage } from 'class-validator';
|
||||
import { BootstrapSetupDto } from './admin/bootstrap.dto.js';
|
||||
|
||||
/**
|
||||
* Boot-time self-check: the global ValidationPipe must be able to SEE the
|
||||
* decorated properties of the DTOs it guards (#1391, #436 class).
|
||||
*
|
||||
* WHY THIS EXISTS. When Nest resolves a @Body() metatype to Object — via
|
||||
* `import type` class erasure (#436), or a dependency graph where the
|
||||
* controller's decorators and the application's route enhancers disagree
|
||||
* (#1391's hypothesized dual-@nestjs/common on a mixed install) — the
|
||||
* ValidationPipe's whitelist treats every property as forbidden. The first
|
||||
* symptom is a 400 on the FIRST bootstrap attempt of a fresh install, the
|
||||
* worst place to discover wiring damage: the operator cannot tell a broken
|
||||
* payload from a broken daemon.
|
||||
*
|
||||
* This check fails LOUD at boot instead: if the pipe cannot see the DTO's
|
||||
* decorated properties, the gateway refuses to start with a named cause.
|
||||
* It catches the whole class — erasure, decorator metadata loss — on every
|
||||
* host, at the moment the damage exists rather than at first use.
|
||||
*
|
||||
* Storage sharing note: class-validator keys its metadata storage on
|
||||
* globalThis, so duplicate package copies do NOT hide metadata (measured,
|
||||
* #1391 diagnosis). What hides it is losing the metatype itself, which is
|
||||
* what this asserts against.
|
||||
*/
|
||||
|
||||
/**
|
||||
* DTOs the global pipe guards, mapped to the properties the whitelist must
|
||||
* admit. Target is the CONSTRUCTOR (the object class itself): class-validator
|
||||
* decorators register metadata keyed on the constructor, and its executor
|
||||
* looks up `object.constructor` (ValidationExecutor.js:50) — the probe
|
||||
* through `prototype` returns zero. Extend when adding DTOs to the app.
|
||||
*/
|
||||
export const PIPE_GUARDED_DTOS: Array<{
|
||||
name: string;
|
||||
target: abstract new (...args: never[]) => unknown;
|
||||
properties: string[];
|
||||
}> = [
|
||||
{
|
||||
name: 'BootstrapSetupDto',
|
||||
target: BootstrapSetupDto,
|
||||
properties: ['name', 'email', 'password'],
|
||||
},
|
||||
];
|
||||
|
||||
export class PipeMetatypeCheckError extends Error {
|
||||
constructor(missing: string[]) {
|
||||
super(
|
||||
'ValidationPipe metatype check failed: ' +
|
||||
missing.join('; ') +
|
||||
'. The global ValidationPipe cannot see decorated DTO properties — ' +
|
||||
'every request body would be rejected as non-whitelisted. ' +
|
||||
'Check for import-type erasure or decorator metadata loss in the ' +
|
||||
'dependency graph (see issues #436, #1391).',
|
||||
);
|
||||
this.name = 'PipeMetatypeCheckError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the pipe's whitelist can see every guarded DTO's decorated
|
||||
* properties. Throws PipeMetatypeCheckError (fail-loud at boot) listing
|
||||
* each miss. Pure function of module state: no I/O, safe to call twice.
|
||||
*/
|
||||
export function assertValidationPipeSeesDtoDecorators(): void {
|
||||
const storage = getMetadataStorage();
|
||||
const missing: string[] = [];
|
||||
|
||||
for (const dto of PIPE_GUARDED_DTOS) {
|
||||
// class-validator records constraints keyed on the DTO's constructor
|
||||
// (decorators run on the class), and its executor resolves them via
|
||||
// object.constructor. A property with no recorded metadata is invisible
|
||||
// to the whitelist — whatever the cause — and fails here.
|
||||
// Signature mirrors ValidationExecutor.js:50 — (constructor, schema, always,
|
||||
// strictGroups, groups?). No schema, always=true, no groups: every
|
||||
// constraint regardless of grouping, which is what the whitelist sees.
|
||||
const metadatas = storage.getTargetValidationMetadatas(dto.target, '', true, false);
|
||||
const decorated = new Set(metadatas.map((m) => m.propertyName));
|
||||
|
||||
for (const property of dto.properties) {
|
||||
if (!decorated.has(property)) {
|
||||
missing.push(
|
||||
`${dto.name}.${property} has no class-validator constraints visible to the pipe`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
throw new PipeMetatypeCheckError(missing);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import 'reflect-metadata';
|
||||
import { type CanActivate, type ExecutionContext, type INestApplication } from '@nestjs/common';
|
||||
import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import request from 'supertest';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
import { TeamsController } from './teams.controller.js';
|
||||
import { TeamsService } from './teams.service.js';
|
||||
|
||||
const teamAlpha = { id: 'team-alpha', name: 'Alpha' };
|
||||
const teamBeta = { id: 'team-beta', name: 'Beta' };
|
||||
|
||||
// user-1 is a member of team-alpha only; admin-1 has role admin.
|
||||
let currentUser: { id: string; role?: string } = { id: 'user-1' };
|
||||
|
||||
const teamsServiceMock = {
|
||||
findAll: vi.fn(() => Promise.resolve([teamAlpha, teamBeta])),
|
||||
findAllForUser: vi.fn((userId: string) =>
|
||||
Promise.resolve(userId === 'user-1' ? [teamAlpha] : []),
|
||||
),
|
||||
findById: vi.fn((id: string) => Promise.resolve([teamAlpha, teamBeta].find((t) => t.id === id))),
|
||||
listMembers: vi.fn(() => Promise.resolve([{ teamId: 'team-alpha', userId: 'user-1' }])),
|
||||
isMember: vi.fn((teamId: string, userId: string) =>
|
||||
Promise.resolve(teamId === 'team-alpha' && userId === 'user-1'),
|
||||
),
|
||||
};
|
||||
|
||||
const authGuard: CanActivate = {
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requestContext = context
|
||||
.switchToHttp()
|
||||
.getRequest<{ user?: { id: string; role?: string } }>();
|
||||
requestContext.user = currentUser;
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
describe('teams endpoints are scoped to membership', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
controllers: [TeamsController],
|
||||
providers: [{ provide: TeamsService, useValue: teamsServiceMock }],
|
||||
})
|
||||
.overrideGuard(AuthGuard)
|
||||
.useValue(authGuard)
|
||||
.compile();
|
||||
|
||||
app = moduleRef.createNestApplication<NestFastifyApplication>(new FastifyAdapter());
|
||||
await app.init();
|
||||
await app.getHttpAdapter().getInstance().ready();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
currentUser = { id: 'user-1' };
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('GET /api/teams returns only the teams the user belongs to', async () => {
|
||||
const response = await request(app.getHttpServer()).get('/api/teams');
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual([teamAlpha]);
|
||||
expect(teamsServiceMock.findAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('GET /api/teams returns every team for an admin', async () => {
|
||||
currentUser = { id: 'admin-1', role: 'admin' };
|
||||
const response = await request(app.getHttpServer()).get('/api/teams');
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual([teamAlpha, teamBeta]);
|
||||
expect(teamsServiceMock.findAllForUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('GET /api/teams/:teamId returns 403 for a non-member', async () => {
|
||||
const response = await request(app.getHttpServer()).get('/api/teams/team-beta');
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
|
||||
it('GET /api/teams/:teamId returns 404 for a missing team', async () => {
|
||||
const response = await request(app.getHttpServer()).get('/api/teams/team-missing');
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it('GET /api/teams/:teamId returns the team for a member', async () => {
|
||||
const response = await request(app.getHttpServer()).get('/api/teams/team-alpha');
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(teamAlpha);
|
||||
});
|
||||
|
||||
it('GET /api/teams/:teamId/members returns 403 for a non-member and members for a member', async () => {
|
||||
const denied = await request(app.getHttpServer()).get('/api/teams/team-beta/members');
|
||||
expect(denied.status).toBe(403);
|
||||
expect(teamsServiceMock.listMembers).not.toHaveBeenCalled();
|
||||
|
||||
const allowed = await request(app.getHttpServer()).get('/api/teams/team-alpha/members');
|
||||
expect(allowed.status).toBe(200);
|
||||
expect(allowed.body).toEqual([{ teamId: 'team-alpha', userId: 'user-1' }]);
|
||||
});
|
||||
|
||||
it('GET /api/teams/:teamId/members/:userId allows a self-lookup on any team', async () => {
|
||||
const response = await request(app.getHttpServer()).get('/api/teams/team-beta/members/user-1');
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ isMember: false });
|
||||
});
|
||||
|
||||
it('GET /api/teams/:teamId/members/:userId denies looking up another user on a foreign team', async () => {
|
||||
const response = await request(app.getHttpServer()).get('/api/teams/team-beta/members/user-2');
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
|
||||
it('an admin can look up any membership', async () => {
|
||||
currentUser = { id: 'admin-1', role: 'admin' };
|
||||
const response = await request(app.getHttpServer()).get('/api/teams/team-alpha/members/user-1');
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ isMember: true });
|
||||
});
|
||||
});
|
||||
@@ -1,30 +1,68 @@
|
||||
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
ForbiddenException,
|
||||
Get,
|
||||
NotFoundException,
|
||||
Param,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||
import { TeamsService } from './teams.service.js';
|
||||
|
||||
type RequestUser = { id: string; role?: string };
|
||||
|
||||
@Controller('api/teams')
|
||||
@UseGuards(AuthGuard)
|
||||
export class TeamsController {
|
||||
constructor(private readonly teams: TeamsService) {}
|
||||
|
||||
@Get()
|
||||
async list() {
|
||||
return this.teams.findAll();
|
||||
async list(@CurrentUser() user: RequestUser) {
|
||||
if (user.role === 'admin') {
|
||||
return this.teams.findAll();
|
||||
}
|
||||
return this.teams.findAllForUser(user.id);
|
||||
}
|
||||
|
||||
@Get(':teamId')
|
||||
async findOne(@Param('teamId') teamId: string) {
|
||||
return this.teams.findById(teamId);
|
||||
async findOne(@Param('teamId') teamId: string, @CurrentUser() user: RequestUser) {
|
||||
return this.getAccessibleTeam(teamId, user);
|
||||
}
|
||||
|
||||
@Get(':teamId/members')
|
||||
async listMembers(@Param('teamId') teamId: string) {
|
||||
async listMembers(@Param('teamId') teamId: string, @CurrentUser() user: RequestUser) {
|
||||
await this.getAccessibleTeam(teamId, user);
|
||||
return this.teams.listMembers(teamId);
|
||||
}
|
||||
|
||||
@Get(':teamId/members/:userId')
|
||||
async checkMembership(@Param('teamId') teamId: string, @Param('userId') userId: string) {
|
||||
async checkMembership(
|
||||
@Param('teamId') teamId: string,
|
||||
@Param('userId') userId: string,
|
||||
@CurrentUser() user: RequestUser,
|
||||
) {
|
||||
// A user may always ask about their own membership; anything else is
|
||||
// team-scoped like the other routes.
|
||||
if (userId !== user.id) {
|
||||
await this.getAccessibleTeam(teamId, user);
|
||||
}
|
||||
const isMember = await this.teams.isMember(teamId, userId);
|
||||
return { isMember };
|
||||
}
|
||||
|
||||
/**
|
||||
* Team-scoped access: admins see any team; everyone else only teams they
|
||||
* are a member of. NotFoundException when the team does not exist and
|
||||
* ForbiddenException when the user lacks access (same convention as the
|
||||
* projects controller).
|
||||
*/
|
||||
private async getAccessibleTeam(teamId: string, user: RequestUser) {
|
||||
const team = await this.teams.findById(teamId);
|
||||
if (!team) throw new NotFoundException('Team not found');
|
||||
if (user.role === 'admin') return team;
|
||||
const isMember = await this.teams.isMember(teamId, user.id);
|
||||
if (!isMember) throw new ForbiddenException('Not a member of this team');
|
||||
return team;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { eq, and, type Db, teams, teamMembers, projects } from '@mosaicstack/db';
|
||||
import { eq, and, inArray, type Db, teams, teamMembers, projects } from '@mosaicstack/db';
|
||||
import { DB } from '../database/database.module.js';
|
||||
|
||||
@Injectable()
|
||||
@@ -56,6 +56,21 @@ export class TeamsService {
|
||||
return this.db.select().from(teams);
|
||||
}
|
||||
|
||||
/**
|
||||
* List only the teams the user is a member of.
|
||||
*/
|
||||
async findAllForUser(userId: string) {
|
||||
const memberRows = await this.db
|
||||
.select({ teamId: teamMembers.teamId })
|
||||
.from(teamMembers)
|
||||
.where(eq(teamMembers.userId, userId));
|
||||
|
||||
const teamIds = memberRows.map((r) => r.teamId);
|
||||
if (teamIds.length === 0) return [];
|
||||
|
||||
return this.db.select().from(teams).where(inArray(teams.id, teamIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a team by ID.
|
||||
*/
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Administrator Guide
|
||||
|
||||
> **Status:** Partially migrated. Current SSO and local upgrade/recovery procedures are available; held procedures are labeled non-operative.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Administrator Operations
|
||||
|
||||
> **Status:** Partially migrated. Procedures explicitly identify whether they are current or held.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Mos Connector Lease Operations — M1
|
||||
|
||||
> **Status:** Held / non-operative.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Upgrade safety and recovery
|
||||
|
||||
> **Supported route:** an already installed `mosaic` CLI using the local PGlite
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Security
|
||||
|
||||
> **Status:** Partially migrated. The SSO provider and Discord ingress security pages are current.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Discord ingress security
|
||||
|
||||
> **Status:** Current Discord behavior only. Telegram shared-contract parity, Matrix channel ingress, and a gateway-wide shared adapter registry are not implemented or are not proven by the current source/tests.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
title: SSO Providers
|
||||
type: runbook
|
||||
audience: admin
|
||||
status: current
|
||||
source_of_truth: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# API Documentation
|
||||
|
||||
> **Status:** Scaffold only. The canonical gateway contract has not yet been migrated into this directory.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Developer Guide
|
||||
|
||||
> **Status:** Partially migrated. Architecture, lease-broker verification, and channel-adapter authoring pages are current; other contributor chapters remain unmigrated.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Architecture
|
||||
|
||||
> **Status:** Partially migrated. The lease-broker security-contract pages below are current references; the remaining architecture pages are still being classified.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Channel protocol architecture
|
||||
|
||||
> **Status:** Current shared type contract and Discord compatibility baseline. The shared gateway registry, Telegram parity, Matrix integration, identity-linking, and multi-surface multiplexing described below are draft or unimplemented.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Compaction observer revocation and runtime generations
|
||||
|
||||
> **Status:** Current contract reference.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Architecture Decisions
|
||||
|
||||
> **Status:** Current decision index. A decision describes an implemented and accepted boundary; draft proposals belong under `rfcs/` or `docs/plans/`.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: record
|
||||
status: active
|
||||
---
|
||||
|
||||
# Mos Runtime Portability M1 — Logical Identity and Fencing
|
||||
|
||||
> **Decision status:** Current implemented decision (M1).
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Authenticated external lease broker protocol
|
||||
|
||||
> **Status:** Current contract reference.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# WI-1 lease broker security notes
|
||||
|
||||
> **Status:** Current contract reference.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Whole mutator-class lease gate
|
||||
|
||||
> **Status:** Current contract reference.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Architecture RFCs
|
||||
|
||||
> **Status:** Current proposal index. RFCs are draft design material and have no operational or implementation authority until an approved decision and implementation evidence supersede them.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: spec
|
||||
status: active
|
||||
---
|
||||
|
||||
# RFC: Optional AI Egress Gateways
|
||||
|
||||
> **Status:** Draft / proposed — not approved, not current, and not integrated.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Channel adapters
|
||||
|
||||
> **Status:** Current shared channel types plus the Discord reference/compatibility implementation. A shared gateway adapter registry, Telegram parity, and Matrix channel integration remain unimplemented or unproven.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
title: Lease-broker operations
|
||||
type: runbook
|
||||
audience: developer
|
||||
status: current
|
||||
source_of_truth: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: tracking
|
||||
status: active
|
||||
---
|
||||
|
||||
# Mission Manifest — MVP
|
||||
|
||||
> Top-level rollup tracking Mosaic Stack MVP execution.
|
||||
|
||||
+247
-1014
File diff suppressed because it is too large
Load Diff
+44
-8
@@ -1,3 +1,9 @@
|
||||
---
|
||||
kind: spec
|
||||
source_of_truth: true
|
||||
status: active
|
||||
---
|
||||
|
||||
# Mosaic Stack Documentation
|
||||
|
||||
This directory is the canonical home for Mosaic Stack product, architecture, API, operations, and delivery documentation.
|
||||
@@ -146,21 +152,51 @@ Every canonical page should:
|
||||
7. Include an owner or maintenance responsibility for operationally sensitive content.
|
||||
8. Link to the relevant book index and related canonical pages.
|
||||
|
||||
Recommended front matter for canonical pages:
|
||||
Required front matter for every canonical page:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: Human-readable page title
|
||||
type: guide
|
||||
audience: developer
|
||||
status: current
|
||||
source_of_truth: false
|
||||
kind: tracking | projection | spec | guide | record | superseded
|
||||
status: active # or: completed | superseded-by: <path>
|
||||
source_of_truth: false # optional, defaults false
|
||||
audience: developer # optional: user | admin | developer | all
|
||||
title: Human-readable page title # optional
|
||||
---
|
||||
```
|
||||
|
||||
Allowed `type` values include `guide`, `concept`, `reference`, `decision`, `rfc`, and `runbook`. Allowed `audience` values are `user`, `admin`, `developer`, and `all`. Allowed `status` values are `current`, `draft`, `deprecated`, and `historical`.
|
||||
`kind` says what the document **is**. One value, required, and it follows the document's content,
|
||||
never its filename: a file named `TASKS.md` whose body says "this is a build plan, not a task
|
||||
tracker" is a `spec`.
|
||||
|
||||
Indexes may omit front matter when their purpose is self-evident. A page with normative authority must explicitly identify the authority it owns and the boundaries of that authority.
|
||||
| kind | rule |
|
||||
| ---------- | ---------------------------------------------------------------- |
|
||||
| tracking | Live state, single-writer. Never a spec |
|
||||
| projection | Generated. Never hand-edited. MUST have a drift test |
|
||||
| spec | How to build one goal or workstream |
|
||||
| guide | Explains use. Decides nothing |
|
||||
| record | What happened. Never authoritative, never updated after the fact |
|
||||
| superseded | Kept for history, and NAMES its replacement |
|
||||
|
||||
`source_of_truth` is a separate boolean because authority is **orthogonal to kind**. A document can
|
||||
be a `spec` and still be the thing everything else answers to;
|
||||
`docs/requirements/native-kanban-sot.md` is exactly that. Folding authority into `kind` forced one
|
||||
field to carry two independent facts, which is why an earlier draft of this contract could not
|
||||
classify that file at all.
|
||||
|
||||
`status` has three values. `active` means in force. `completed` means the work the document
|
||||
describes landed and the document is now finished rather than stale; executed implementation plans
|
||||
take this. `superseded-by: <path>` replaces `status` entirely and names the replacement.
|
||||
|
||||
**This contract covers `.md` files only.** It is not an omission: a YAML document cannot carry YAML
|
||||
front matter. The repository's own `[email protected]` throws `Source contains multiple documents` on a
|
||||
front-mattered `.yaml`, and `parseNorthStar` (`packages/mosaic/src/commands/fleet.ts:242`) is a live
|
||||
consumer that would break. `.yaml` sources declare their own kind inside the document or not at all.
|
||||
|
||||
A `parent` field is planned and is deliberately not yet required; it lands once the docs flatten
|
||||
settles the paths it would point at.
|
||||
|
||||
Indexes may omit front matter when their purpose is self-evident. A page with normative authority
|
||||
must explicitly identify the authority it owns and the boundaries of that authority.
|
||||
|
||||
## Obsidian and link conventions
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
kind: spec
|
||||
status: active
|
||||
---
|
||||
|
||||
# Mosaic Stack Roadmap
|
||||
|
||||
Companion to [docs/PRD.md](./PRD.md). Governed by the D11 rule: **every planned
|
||||
phase appears here from day one, even as a placeholder** — nothing exists only
|
||||
in heads. A phase marked _placeholder_ is a commitment to design it, not a
|
||||
design; scoping one requires its own PRD section or requirements doc plus
|
||||
review.
|
||||
|
||||
Phases are product phases. The in-flight platform workstreams (KBN-100/101
|
||||
kanban SOT implementation, FCM #758, FCOM #766, TESS, RI #1275, and the other
|
||||
Part II contracts in the PRD) run as parallel tracks under their own issues
|
||||
and are prerequisites where noted.
|
||||
|
||||
| Phase | Scope | Status |
|
||||
| ----- | ------------------------------------------------------------------------------ | ----------------------------------------- |
|
||||
| P0 | Current state on `next`: read-only dashboard, chat, auth/SSO login, admin tabs | shipped, evolving |
|
||||
| P1 | **v1 slice** (PRD Part I §9) | next up |
|
||||
| P2 | Connectors + comms + wizard expansion | placeholder |
|
||||
| P3 | Full onboarding profile + M365 | placeholder |
|
||||
| P4 | Enterprise mode + one-way conversion | placeholder |
|
||||
| P5 | Federation | placeholder (deliberately undesigned, D3) |
|
||||
|
||||
## P0 — current state
|
||||
|
||||
What exists on `next` today: web dashboard (login/register/SSO, chat,
|
||||
read-only projects/tasks, settings, admin user/system-health tabs), the
|
||||
Gateway, the CLI-first framework tooling, and the fleet control plane. The
|
||||
webUI audit (USC estate, webui-audit lane) measures the gap between this and
|
||||
P1.
|
||||
|
||||
## P1 — v1 slice (D11)
|
||||
|
||||
1. Standalone onboarding wizard: system/company name, component choices,
|
||||
initial user, initial estate + project, seeded examples, re-runnable.
|
||||
2. Hierarchy core: company → estate → project → workspace → kanban, read-only
|
||||
task bubble-up (kanban SOT Amendment A1 is the schema contract).
|
||||
3. Basic RBAC on the hierarchy.
|
||||
4. Minimal agent enrollment: one harness, API key, name/persona.
|
||||
|
||||
Prerequisites: KBN-100/101 schema foundation; the D8 tool inventory and
|
||||
webUI→tool mapping (any missing tool is built first, D12).
|
||||
|
||||
## P2 — connectors + comms + wizard expansion (placeholder)
|
||||
|
||||
Email and drive connectors (Gmail/IMAP, Google Drive/OneDrive/Dropbox) with
|
||||
granular agentic-access consent; comms integrations (Matrix/Discord/Slack)
|
||||
including agent auto-enroll. Wizard gains the corresponding tabs (D4), plus
|
||||
the D4 capabilities deferred out of P1's minimal slice: expanded agent
|
||||
enrollment (OAuth login, multi-account, model choice with recommendation,
|
||||
account assignment, comms auto-enroll) and the Standalone SSO/OIDC
|
||||
configuration tab.
|
||||
|
||||
## P3 — full onboarding profile + M365 (placeholder)
|
||||
|
||||
Complete user onboarding profile (communication-style capture, optional
|
||||
voice-matching interview) under the D14 custody rule; M365 connectors,
|
||||
available to both deployment modes as ordinary connectors (same consent model
|
||||
as the P2 connector class). The Enterprise install flow's M365 prominence
|
||||
(D4) arrives with the Enterprise phase, P4.
|
||||
|
||||
## P4 — Enterprise mode + conversion (placeholder)
|
||||
|
||||
Enterprise install flow (org chart, RBAC focus, immediate OIDC, SSO
|
||||
prominent); per-user brains with architectural isolation (D14); Vault
|
||||
required; the one-way Standalone → Enterprise conversion (D3).
|
||||
|
||||
## P5 — federation (placeholder)
|
||||
|
||||
Connecting deployments: system-level config, assigned users, rights and
|
||||
data-access control, trusts with boundaries, strict data access, exfiltration
|
||||
monitoring. Explicitly not designed yet (D3); nothing in earlier phases may
|
||||
foreclose it. Requires its own PRD + threat model before any scoping.
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
> **Status:** Partially migrated. The quickstart, web-dashboard reference, and Discord conversation workflow are current.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
title: Mosaic Stack Quickstart
|
||||
type: guide
|
||||
audience: user
|
||||
status: current
|
||||
source_of_truth: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
title: Mosaic web dashboard
|
||||
type: guide
|
||||
audience: user
|
||||
status: current
|
||||
source_of_truth: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Discord conversations
|
||||
|
||||
> **Status:** Current Discord workflow for an administrator-provisioned, authorized guild channel.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: tracking
|
||||
status: active
|
||||
---
|
||||
|
||||
# Mission Manifest — Federation v1
|
||||
|
||||
> Persistent document tracking full mission scope, status, and session history.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: spec
|
||||
status: active
|
||||
---
|
||||
|
||||
# PRD — Mosaic Fleet Suite (init, configure, operate)
|
||||
|
||||
> **Workstream:** W-FLEET (Fleet) under mission `mvp-20260312` · **Phase:** 3→4 productization
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: spec
|
||||
status: active
|
||||
---
|
||||
|
||||
# PRD — Fleet Phase 2: Operator Observability
|
||||
|
||||
> **Workstream:** W-FLEET under `mvp-20260312` · **Phase:** 2
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Desired, Derived, and Observed Fleet State
|
||||
|
||||
## One writable authority
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Generated Environment Launch Chain
|
||||
|
||||
The launcher consumes validated data, not shell configuration.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Fleet Identity, Class, and Runtime
|
||||
|
||||
Each roster field has one job. Do not use names or model strings as authority shortcuts.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Fleet Role Authority and Leases
|
||||
|
||||
Role content describes behavior; protected authority is immutable code metadata derived only from the canonical class.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Configure an Interaction Instance
|
||||
|
||||
An interaction instance is a configurable local roster member with canonical class: interaction and matching tool_policy: interaction. “Tess” may be used as a display alias, but neither that alias nor the stable name is required or authority-bearing.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Configure a Validator Instance
|
||||
|
||||
A validator instance is a configurable local roster member with canonical class: validator and matching tool_policy: validator. “Ultron” may be used as a display alias, but it is not a required identity, class alias, product name, or source of authority.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Create, Inspect, Update, and Delete a Local Fleet Agent
|
||||
|
||||
Use the local roster-v2 control plane only. These commands change desired state and derived environment projections; they never start, stop, reconcile, inspect, or otherwise act on systemd, tmux, sessions, or runtimes.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Customize Fleet Roles
|
||||
|
||||
Mosaic resolves persona contracts through two layers:
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Safely Reconcile and Control a Local Fleet Agent
|
||||
|
||||
Use the canonical local roster-v2 command surface:
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Executable Fleet Example, Profile, and Service-Preset Dispositions
|
||||
|
||||
**Issue:** #758 · **Card:** FCM-M1-003 · **Status:** M1 executable disposition evidence
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Legacy Fleet Class Aliases
|
||||
|
||||
Fleet class compatibility is intentionally narrow. The shared resolver accepts exactly three legacy
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Previewing a Fleet Roster v1-to-v2 Migration
|
||||
|
||||
**Issue:** #758 · **Card:** FCM-M4-001 · **Effect boundary:** preview only
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Fleet Configuration Backup and Restore Boundary
|
||||
|
||||
**Issue:** #758 · **Card:** FCM-M4-001
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Environment Quarantine Operations
|
||||
|
||||
Legacy <name>.env is input evidence, never current launch authority. Projection preparation classifies it deterministically:
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Reconcile and Recover a Local Fleet
|
||||
|
||||
## Safe sequence
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Systemd and tmux Troubleshooting
|
||||
|
||||
Start with read-only mosaic fleet status, `doctor`, and `verify`. Do not manually adopt, rename, terminate, or recreate sessions while ownership is ambiguous.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Upgrade and Installed-Asset Drift
|
||||
|
||||
Fleet source assets and installed assets can differ after an update, but FCM-M5-001 does not add a trustworthy source-versus-installed revision detector or refresh command. Do not infer freshness from checkout presence, timestamps, generated environment files, running sessions, or a ready migration preview.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Local Fleet Agent Mutations
|
||||
|
||||
FCM-M2-002 provides local roster-v2 create, get, update, delete, and plan operations. They only change desired state and derived environment projections. They never start, stop, inspect, reconcile, or otherwise act on runtimes, systemd units, tmux sessions, or heartbeats.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Fleet Control-Plane CLI
|
||||
|
||||
The local desired-state surface is mosaic fleet. It is distinct from the gateway-backed mosaic agent catalog and from legacy compatibility commands that act on roster v1.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Fleet Generated Environment Boundary
|
||||
|
||||
**Card:** FCM-M2-001 · **Issue:** #758 · **Status:** merged contract
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Local Fleet Lifecycle Transitions
|
||||
|
||||
Roster-v2 `lifecycle.enabled` and `lifecycle.desired_state` are the only persisted lifecycle authority. Systemd, tmux, generated environment, and heartbeat state are derived or observed.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Fleet Role Classes and Authority
|
||||
|
||||
A fleet role class is a machine identity resolved from the persona library. Resolution uses the
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Fleet Roster v2 Structural Contract
|
||||
|
||||
**Status:** FCM-M1-001 local-tmux structural compiler contract. This document describes parsing,
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Local Fleet Status and Drift
|
||||
|
||||
mosaic fleet status [<name>], `verify`, and `doctor` are observational roster-v2 commands. They emit one JSON result and do not write projections, mutate desired state, operate lifecycle, or change tmux.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Deployment Guide
|
||||
|
||||
> **Status: non-operative for PostgreSQL, federated, and bare-metal production.** The checked-in
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Mosaic Stack — Developer Guide
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Local Fleet Canary
|
||||
|
||||
The local fleet canary runs a small tmux-backed Mosaic agent fleet on an
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Migrating to the Federated Tier
|
||||
|
||||
> **KBN-101-07 ownership:** This active documentation is a **non-operative KBN-101
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: tracking
|
||||
status: active
|
||||
---
|
||||
|
||||
# Mission Manifest — Mosaic Native Kanban and Canonical Task SOT P0–P3
|
||||
|
||||
**Mission status:** CANON INDEPENDENTLY APPROVED; publication in progress under issue [#751](https://git.mosaicstack.dev/mosaicstack/stack/issues/751)
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: spec
|
||||
status: active
|
||||
---
|
||||
|
||||
# Native Kanban/SOT P0–P3 — Dependency-Ordered Build Slices
|
||||
|
||||
**Status:** CANON INDEPENDENTLY APPROVED; PUBLICATION IN PROGRESS
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: spec
|
||||
status: active
|
||||
---
|
||||
|
||||
# CI Queue Guard Purpose Semantics
|
||||
|
||||
- **Issue:** #1146
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: spec
|
||||
status: completed
|
||||
---
|
||||
|
||||
# CI Queue Guard Purpose Semantics Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: spec
|
||||
status: active
|
||||
---
|
||||
|
||||
# Documentation Catalog and Truth Audit Plan
|
||||
|
||||
**Task:** DOCS-IA-002
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: spec
|
||||
status: active
|
||||
---
|
||||
|
||||
# Documentation Information Architecture Design
|
||||
|
||||
**Status:** Approved
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: spec
|
||||
status: completed
|
||||
---
|
||||
|
||||
# Documentation Structure README Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
@@ -0,0 +1,429 @@
|
||||
---
|
||||
kind: tracking
|
||||
status: active
|
||||
---
|
||||
|
||||
# W4 — document contract worklist
|
||||
|
||||
Companion to `2026-08-20_stack-docs-flatten-and-alignment.md`. That document proposes the
|
||||
contract; this one records what was applied, what was held, and what still needs a decision.
|
||||
|
||||
Measured on `origin/next` at `63069149`. Author: veronica. Review: fred (Gate-16, author is
|
||||
not reviewer), then a pi seat for the adversarial pass.
|
||||
|
||||
## What "live" means here
|
||||
|
||||
All `*.md` under `docs/`, minus `docs/archive/`, minus `docs/_old_structure/`. That is **127**
|
||||
files. The flatten plan says 130; the arithmetic does not close (318 total = 134 archive +
|
||||
57 `_old_structure` + 127 live, not 130).
|
||||
|
||||
## Applied
|
||||
|
||||
**These are the FIRST-PASS counts and they are superseded. The current tree is counted in
|
||||
"Verification arithmetic, re-closed" at the end of this document.** They are left standing rather
|
||||
than overwritten, for the same reason every other correction here is: a record of what a pass
|
||||
produced is worth more than a number silently updated to still look right.
|
||||
|
||||
| bucket | count | note |
|
||||
| --------------------------- | ----- | -------------------------------------- |
|
||||
| stamped `kind` + `status` | 107 | this pass |
|
||||
| held, operator judgement | 17 | section "Needs a decision" below |
|
||||
| held, cites the moving path | 3 | the three `SUPERSEDED` TASKS.md stamps |
|
||||
| held, generated file | 1 | `docs/fleet/NORTH_STAR.md`, see below |
|
||||
|
||||
128 live `.md` under `docs/`, which is the 127 baseline plus this document. 107 + 17 + 3 + 1 = 128.
|
||||
|
||||
Kinds: 53 `guide`, 34 `record`, 13 `spec`, 7 `tracking`. Status: 105 `active`, 2 `completed`.
|
||||
|
||||
After fred's Q1 ruling stamped `docs/README.md`, one file moved from the held bucket to the stamped
|
||||
one and nothing else changed: **108 stamped + 16 + 3 + 1 = 128**, kinds 54 `guide`, 34 `record`,
|
||||
13 `spec`, 7 `tracking`, status 106 `active`, 2 `completed`. Two files carry
|
||||
`source_of_truth: true`, `docs/README.md` and `docs/requirements/native-kanban-sot.md`.
|
||||
|
||||
`parent` is **not** applied. It points at `docs/fleet/NORTH_STAR.yaml`, which the flatten moves
|
||||
to `docs/NORTH_STAR.yaml`. Stamping it now means re-pointing 127 files by hand later. It lands
|
||||
after the move, in one pass, with the post-move path.
|
||||
|
||||
## Held: the three superseded stamps
|
||||
|
||||
`docs/TASKS.md:5`, `docs/federation/TASKS.md:5`, and `docs/fleet/TASKS.md:5` each carry a W1
|
||||
stamp of the form:
|
||||
|
||||
> **STATUS: SUPERSEDED — 2026-08-20.** kind `tracking` · superseded by `docs/fleet/NORTH_STAR.yaml`
|
||||
|
||||
Two problems, both real:
|
||||
|
||||
1. **The tooling cannot read them.** That line is a blockquote below the H1, not YAML front
|
||||
matter. Plan section 6 check 5 ("every live document has a header; no document is
|
||||
unclassified") parses front matter, so all three read as unclassified. The control that the
|
||||
parse itself works is `docs/fleet/FLEET-DOCTRINE.md:3`, which is genuine front matter.
|
||||
2. **They cite the moving path.** Three of the six citations of `docs/fleet/NORTH_STAR.yaml`
|
||||
are these stamps. Converting them before the flatten lands makes them stale on merge.
|
||||
|
||||
Converted after the flatten, not before.
|
||||
|
||||
## Held: the one generated file
|
||||
|
||||
`docs/fleet/NORTH_STAR.md` is a `projection` and was stamped in the first pass. The stamp was
|
||||
**reverted before review**, because stamping it is self-contradictory in a way that is not
|
||||
merely theoretical:
|
||||
|
||||
- The contract says a `projection` is "Generated. Never hand-edited." Adding front matter by
|
||||
hand is a hand edit of a generated file, and the file's own banner says
|
||||
"**Generated file — do not edit by hand.**"
|
||||
- `renderNorthStarMarkdown()` at `packages/mosaic/src/commands/fleet.ts:373` emits the H1 as
|
||||
its first line and no front matter at all.
|
||||
- `fleet-north-star.spec.ts:110-114` asserts full-string equality between the renderer's output
|
||||
and the committed file: `expect(rendered).toBe(committed)`. Before the stamp, line 1 of the
|
||||
committed file was `# Mosaic Fleet — NORTH STAR`, matching the renderer. After it, line 1 was
|
||||
`---`. The assertion fails.
|
||||
|
||||
So the header for a projection cannot live in the file. It has to be emitted by
|
||||
`renderNorthStarMarkdown()`, which is a code change and belongs in the flatten PR alongside the
|
||||
`resolveNorthStarPaths()` fix, not in a documentation pass.
|
||||
|
||||
This generalises: **the contract as written cannot classify any generated document without a
|
||||
matching change to its generator.** `NORTH_STAR.md` is the only projection today, so the cost
|
||||
is one function. It will not stay one.
|
||||
|
||||
## Needs a decision
|
||||
|
||||
Nineteen rows. Seventeen are the plan's section 9 list, minus `docs/fleet/north-star.md`
|
||||
(renamed to `FLEET-DOCTRINE.md` by W1, so the row is closed) and minus the two `.yaml` rows,
|
||||
which are a different problem — see the next section. Two rows are new, found while
|
||||
classifying.
|
||||
|
||||
Fill the `kind` column with a value from the contract, or `superseded-by: <path>`.
|
||||
|
||||
| path | proposed | why it is not mechanical |
|
||||
| --------------------------------------------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `docs/README.md` | `guide` | It also **prescribes** the competing front-matter convention (see below). Whatever kind it gets, its body needs an edit. |
|
||||
| `docs/SITEMAP.md` | `guide` or `projection` | If it is generated from the tree it is a projection and needs a drift test. If hand-maintained it is a guide that goes stale silently. Nobody has said which. |
|
||||
| `docs/federation/SETUP.md` | `guide` | Reads as a guide. Federation tier status is the open part: if the tier is shelved this is `superseded`. |
|
||||
| `docs/fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md` | `record` | An acceptance checklist is evidence of a past gate, which is `record`. But if it is still being checked against, it is `tracking`. |
|
||||
| `docs/fleet/FLEET-LAUNCH.md` | `guide` | Runbook. Depends on whether `launch-seat.sh` is deprecated in favour of `mosaic fleet` (campaign W3). If so, `superseded`. |
|
||||
| `docs/fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md` | `record` | An inventory of dispositions taken. `record` unless dispositions are still pending, which the title implies they were once. |
|
||||
| `docs/fleet/README.md` | `guide` | Section index. Low risk; listed only because the plan lists it. |
|
||||
| `docs/fleet/backlog-conventions.md` | `guide` | Conventions decide things, and `guide` explicitly decides nothing. May be `spec`. |
|
||||
| `docs/fleet/f4-matrix-connector.md` | `spec` | F4 is a workstream. Whether it is live or abandoned decides `spec` versus `superseded`. |
|
||||
| `docs/native-kanban-sot/DOCUMENTATION-CHECKLIST.md` | `record` | Same question as the fleet IA checklist. |
|
||||
| `docs/native-kanban-sot/INDEX.md` | `guide` | Index of the canon. If it is generated from the canon it is a projection. |
|
||||
| `docs/native-kanban-sot/KBN-010-THREAT-AUTH-CONSTRAINT-GATE.md` | `spec` | 415 lines of normative gate. `spec` fits; confirm it is not superseded by the shared contract. |
|
||||
| `docs/native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md` | `spec` | As above. |
|
||||
| `docs/native-kanban-sot/KBN-101-ENVELOPE-A.md` | `spec` | Title says "v6, FINAL". If v6 supersedes v1-v5 elsewhere, those need `superseded-by` pointing here. |
|
||||
| `docs/native-kanban-sot/SHARED-CONTRACT.md` | `spec` | "Remediated Shared Contract v1". Same versioning question. |
|
||||
| `docs/release-integrity/probe-inventory.md` | `record` | An inventory. `tracking` if probes are still being added to it. |
|
||||
| `docs/webui/PHASE-P-STRUCTURE.md` | `spec` | Zero inbound references (plan section 5.4). Either wire it in or mark it superseded; the kind is the smaller question. |
|
||||
| `docs/native-kanban-sot/TASKS.md` | **conflict** | The file says of itself: "This file is a build plan, not a task tracker, and is NOT superseded." The contract says `tracking` is "live state, single-writer (manifests, `TASKS.md`). **Never a spec**." The file claims to be the thing its name forbids. Either the file is wrong or the contract's `TASKS.md` shorthand is. |
|
||||
| `docs/requirements/native-kanban-sot.md` | **conflict** | Plan section 5.2 says if it is hand-authored canon it is `source-of-truth`. The contract's own table says `source-of-truth` is **machine-readable**. This is prose markdown with normative MUST/MUST NOT, RATIFIED 2026-07-14, decision owner Jason. It cannot satisfy both rules. Either it is a `spec` or the machine-readable criterion is wrong. |
|
||||
|
||||
The last two are not slow rows, they are contradictions inside the contract. They want an
|
||||
answer about the contract, not about the file.
|
||||
|
||||
## The contract collides with an existing one
|
||||
|
||||
`docs/README.md` lines 150-160 already document a front-matter convention, with its own
|
||||
allowed values:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: Human-readable page title
|
||||
type: guide # guide | concept | reference | decision | rfc | runbook
|
||||
audience: developer # user | admin | developer | all
|
||||
status: current # current | draft | deprecated | historical
|
||||
source_of_truth: false
|
||||
---
|
||||
```
|
||||
|
||||
Adoption is 4 of 127 files: `docs/ADMIN-GUIDE/security/sso-providers.md`,
|
||||
`docs/DEVELOPER-GUIDE/testing/lease-broker-operations.md`,
|
||||
`docs/USER-GUIDE/getting-started/quickstart.md`, `docs/USER-GUIDE/product/web-dashboard.md`.
|
||||
|
||||
`status` is in both schemas with **disjoint vocabularies**. `type` and `kind` are two names for
|
||||
one idea with different value sets. `source_of_truth: false` is a boolean spelling of
|
||||
`kind: source-of-truth`.
|
||||
|
||||
What this pass did, and it is a decision someone should ratify or reverse: the new contract
|
||||
wins. Those 4 files had `status: current` rewritten to `status: active` and gained `kind:`.
|
||||
Their `title`, `type`, `audience` and `source_of_truth` keys were left alone. Nothing reads any
|
||||
of them — `git grep source_of_truth` outside `docs/` returns zero hits — so no consumer broke.
|
||||
|
||||
`docs/README.md` still prescribes the old convention. It is an operator row above, so this pass
|
||||
did not edit it. Until it is edited, the repository documents two conflicting header
|
||||
conventions and points authors at the one being retired.
|
||||
|
||||
## The contract has no form for a YAML document
|
||||
|
||||
Two of the plan's 20 rows are not markdown: `docs/fleet/examples/roster-v2.yaml` and
|
||||
`docs/openapi-tess.yaml`. Front matter is a markdown convention. A `.yaml` file can carry a
|
||||
leading `---` document, but then it is two YAML documents and every existing parser of that
|
||||
file sees a change.
|
||||
|
||||
This is not an edge case. It applies to **`docs/fleet/NORTH_STAR.yaml`, the source of truth
|
||||
itself**, which is the one file the contract most needs to classify. Section 6 check 5 says no
|
||||
document is unclassified. As written, the source of truth cannot comply.
|
||||
|
||||
Options, none of them chosen here: exclude `.yaml` from the contract and say so; carry their
|
||||
metadata in a sidecar; or add a top-level `kind:` key inside the YAML body rather than as front
|
||||
matter, which for `NORTH_STAR.yaml` is a schema change.
|
||||
|
||||
## Method, and what it cannot tell you
|
||||
|
||||
Classification is per-file, by title and path, recorded with a confidence. It is not a regex
|
||||
sweep. The plan's own warning stands and is why the 19 rows above are held rather than guessed:
|
||||
the first classifier pass classed a RATIFIED requirements document as a projection.
|
||||
|
||||
Rows marked `med` in the working manifest and not listed above: the five `docs/plans/*` specs,
|
||||
`docs/fleet/migration/example-profile-disposition.md`, the one ADR
|
||||
(`decisions/mos-runtime-portability-m1.md`, called `record` because an ADR records a decision
|
||||
taken), and the one RFC (`rfcs/optional-ai-egress-gateways.md`, called `spec` because it
|
||||
proposes work not yet built). Those eight are stamped and are the most likely to be wrong.
|
||||
|
||||
## Verification
|
||||
|
||||
- 103 of 103 files parse with the expected `kind` and `status` in front matter.
|
||||
- The check was shown to reject a wrong kind before it was trusted (asserting
|
||||
`kind: record` on a file stamped `guide` fails).
|
||||
- The whole diff removes 4 lines, all of them `status: current`.
|
||||
- 24 files untouched, matching 19 + 3 + 1 + 1.
|
||||
|
||||
## Response to the W5 adversarial pass
|
||||
|
||||
vision ran a refute-first pass on this branch at `37cd00e` from a fresh worktree. Three of its
|
||||
points changed the branch. Everything is re-measured here before being acted on; where my
|
||||
measurement disagrees with its stated evidence I say so.
|
||||
|
||||
### 1. `docs/fleet/NORTH_STAR.md` stamp reverted (`bea47543`)
|
||||
|
||||
vision raised this as **latent** and scoped to the flatten PR. It is **live in this PR**, so it
|
||||
could not wait.
|
||||
|
||||
`fleet-north-star.spec.ts:110-114` is a drift test that reads the committed file from disk and
|
||||
asserts full-string equality against `renderNorthStarMarkdown()`, whose first emitted line is the
|
||||
H1 and which emits no front matter. Stamping changed line 1 from the H1 to `---`.
|
||||
|
||||
CI 2589 confirms it directly, no longer by construction:
|
||||
|
||||
```
|
||||
× renderNorthStarMarkdown > matches the committed NORTH_STAR.md projection (regenerate if this fails)
|
||||
→ expected '# Mosaic Fleet — NORTH STAR\n\n> **Ge…' to be '---\nkind: projection\nstatus: active…'
|
||||
```
|
||||
|
||||
Reverted to `origin/next` verbatim. `git diff origin/next -- docs/fleet/NORTH_STAR.md` is 0 lines;
|
||||
control on `docs/fleet/reference/cli.md` returns 13, so the diff command does report differences.
|
||||
|
||||
The consequence is a contract-level one, recorded in the worklist: **the contract as written
|
||||
cannot classify any generated document without a matching change to its generator.** A `projection`
|
||||
is "Generated. Never hand-edited," so its header has to come out of the renderer. That is a code
|
||||
change and belongs in the flatten PR beside the `resolveNorthStarPaths()` fix. vision's
|
||||
recommendation, and I agree with it.
|
||||
|
||||
Counts: stamped 104 → **103**. Untouched 23 → **24**.
|
||||
|
||||
### 2. The `docs/` qualifier (vision's C1)
|
||||
|
||||
Stated as asked. **"127 live documentation files" is true for `docs/` only.** Definition: all
|
||||
`*.md` under `docs/`, minus `docs/archive/` and `docs/_old_structure/`. 318 total = 134 archive +
|
||||
57 `_old_structure` + 127 live.
|
||||
|
||||
Repo-wide the phrase undercounts: 21 live markdown files sit outside `docs/` and outside any named
|
||||
exclusion (17 under `guides/`, plus `README.md`, `AGENTS.md`, `CLAUDE.md`, `REPORT-A1207.md` at
|
||||
root). This PR does not stamp them and does not claim to.
|
||||
|
||||
### 3. `format` failure, and what it says about the header (`8a55c041`)
|
||||
|
||||
CI 2589 also failed `prettier --check` on **exactly one file**: the plan document I hand-wrote.
|
||||
Reproduced locally, rc=1, same single file. Fixed; the diff is 31 lines in, 31 out, all table
|
||||
column padding.
|
||||
|
||||
Worth stating for the review rather than burying: **the other 103 stamped documents pass
|
||||
`prettier --check` unchanged.** The `---\nkind:\nstatus:\n---` block is prettier-clean as applied.
|
||||
The formatting failure was in my prose, not in the contract header.
|
||||
|
||||
### 4. R1: the evidence inverts, the finding gets stronger
|
||||
|
||||
vision asks for a `kind` change on `docs/DEVELOPER-GUIDE/architecture/channel-protocol.md`, on the
|
||||
grounds that it "carries 7 normative MUSTs" while the contract says `guide` "decides nothing."
|
||||
|
||||
**The MUST count does not hold.** Uppercase RFC2119 terms (`MUST`, `MUST NOT`, `SHOULD`,
|
||||
`SHOULD NOT`, `SHALL`, `MAY`, `REQUIRED`) in that file: **0 lines**. Control: the identical grep
|
||||
returns 25 lines in `docs/requirements/native-kanban-sot.md`, so it finds them where they exist.
|
||||
The seven lowercase "must" occurrences all _disclaim_ authority rather than assert it: "must not be
|
||||
used as instructions", "must not be treated as current behavior", "must remain planned work", "must
|
||||
first specify", "before it can become architecture guidance." The file's own banner reads "it is
|
||||
not a runtime registry, an API contract, a requirements document."
|
||||
|
||||
**The citation half holds, and is larger than stated.** vision's line numbers are exact. I
|
||||
restated them earlier with wrong directories, which is worth naming because it is the same method
|
||||
failure fred and I already wrote up as C3 in the flatten plan: I matched on basename and assumed
|
||||
the path. The real ones, plus the two vision did not list:
|
||||
|
||||
| citing document | line | words used |
|
||||
| ------------------------------------------------------- | ---- | --------------------------------------------------- |
|
||||
| `docs/ADMIN-GUIDE/security/discord-ingress.md` | 141 | "**canonical** shared-contract and parity boundary" |
|
||||
| `docs/DEVELOPER-GUIDE/integrations/channel-adapters.md` | 28 | "The **canonical** architecture summary is" |
|
||||
| `docs/DEVELOPER-GUIDE/integrations/channel-adapters.md` | 183 | "**Canonical** channel protocol architecture" |
|
||||
| `docs/USER-GUIDE/workflows/discord-conversations.md` | 127 | "current shared types ... explicit parity boundary" |
|
||||
| `docs/SITEMAP.md` | 54 | index entry |
|
||||
| `docs/DEVELOPER-GUIDE/architecture/README.md` | 18 | index entry |
|
||||
|
||||
So the tension is real. Three live documents outside the two indexes cite it, across four
|
||||
citations, and three of those four use the word "canonical" for a document that spends its own
|
||||
banner denying it is canonical. **It is just not a MUST problem, and that
|
||||
changes what the fix is.** If the file is telling the truth about itself, the three "canonical"
|
||||
citations are wrong and the edit belongs in those three files, not in this one's `kind`.
|
||||
|
||||
**Left as `kind: guide` in this PR and flagged for the reviewer's call.** Restamping on evidence
|
||||
that inverts on reading would be worse than leaving it stamped and named.
|
||||
|
||||
### Unchanged
|
||||
|
||||
vision's C2 (no consumer), C4 (holding `parent` blocks nothing) and C5 (a front-mattered `.yaml`
|
||||
throws in `YAML.parse`) all reverified. C1's arithmetic closes at the stated ref.
|
||||
|
||||
## fred's six decisions, applied
|
||||
|
||||
Ruled on PR #1350 as comment 23693. Each is applied here; each is his call, not mine, and any of
|
||||
them is one line to reverse.
|
||||
|
||||
| # | decision | applied as |
|
||||
| --- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| A | New contract wins; `docs/README.md` is rewritten in this PR and the 4 old-schema files convert in the same pass | `docs/README.md:149-190` rewritten; `type:` dropped from the 4 files, `title`/`audience`/`source_of_truth` kept |
|
||||
| B | `source-of-truth` leaves the `kind` enum and returns as an orthogonal boolean | enum is now 6 values; `docs/requirements/native-kanban-sot.md` stamped `kind: spec` + `source_of_truth: true` |
|
||||
| C | `status` gains a third value, `completed` | the two executed plans take it (evidence below) |
|
||||
| D | Kind follows content, never filename | `docs/native-kanban-sot/TASKS.md` stamped `kind: spec`, because its body says "a build plan, not a task tracker" |
|
||||
| E | The contract covers `.md` only, stated as a decision rather than left as a gap | written into `docs/README.md` with vision's `YAML.parse` measurement as the reason |
|
||||
| F | `channel-protocol.md` becomes `spec` | applied, with one correction and one consequence below |
|
||||
|
||||
### C: evidence the two plans are complete
|
||||
|
||||
Neither plan self-declares completion, so this is measured from the artifacts rather than taken
|
||||
from the documents:
|
||||
|
||||
- `2026-08-10-ci-queue-purpose-implementation.md` — the shipped guard carries the flag the plan
|
||||
specifies: `ci-queue-wait.sh --purpose push|merge`, exercised in this session at rc=0.
|
||||
- `2026-08-10-docs-structure-readme.md` — every section the plan specifies exists in
|
||||
`docs/README.md` today, including the Obsidian conventions and the source-of-truth precedence
|
||||
block. This PR is editing the artifact that plan produced.
|
||||
|
||||
### F: the MUST count does not hold, and the fix changes shape
|
||||
|
||||
Applied as ruled. But the ruling's stated grounds are half wrong, and the half that survives points
|
||||
somewhere else, so this is the one to look at again.
|
||||
|
||||
**Zero** uppercase RFC2119 terms in `channel-protocol.md`, not seven. Control: the identical grep
|
||||
returns 25 lines in `docs/requirements/native-kanban-sot.md`. The seven lowercase "must"
|
||||
occurrences all _disclaim_ authority: "must not be used as instructions", "must not be treated as
|
||||
current behavior", "must remain planned work".
|
||||
|
||||
**The citation half holds and is bigger than stated** (table in the section above).
|
||||
|
||||
**The consequence of applying F:** the file is now stamped `spec` while its own banner says "it is
|
||||
not a runtime registry, an API contract, a requirements document." Header and body now contradict
|
||||
each other, which is the defect this pass exists to remove. Either the banner is edited in this PR,
|
||||
or the three documents calling it canonical are the ones that are wrong. That is a content call and
|
||||
it is left to the reviewer rather than folded into a stamping pass.
|
||||
|
||||
## Q1 — the one question this pass cannot answer for itself
|
||||
|
||||
`docs/README.md` now **prescribes** the document contract, and it is the only live document under
|
||||
`docs/` with no `kind`. It is still on the operator-held list, so it is left unstamped.
|
||||
|
||||
By decision B it is arguably `kind: spec` with `source_of_truth: true` for the subject "document
|
||||
contract". The reason this is not applied unilaterally: it decides which document outranks the
|
||||
other when `docs/README.md` and `docs/plans/2026-08-20_stack-docs-flatten-and-alignment.md` disagree
|
||||
about the contract, and they already differ (the plan's enum has 7 values, the README's now has 6).
|
||||
That is an authority question, not a classification one.
|
||||
|
||||
## The old schema DID have a consumer, and CI found it
|
||||
|
||||
vision's C2 concluded "no consumer found" after searching by parsing primitive rather than by key
|
||||
name. fred's decision A rested on "no installed base to protect." I accepted both. **All three of
|
||||
us were wrong, and the full test suite is what proved it.**
|
||||
|
||||
`packages/mosaic/src/installation-documentation.spec.ts:39` asserted:
|
||||
|
||||
```ts
|
||||
expect(markdown).toMatch(/^---\n[\s\S]*?\nstatus: current\n[\s\S]*?\n---\n/);
|
||||
```
|
||||
|
||||
That is a raw regex over the markdown text, not a front-matter parse and not a key lookup, which is
|
||||
exactly why a search organised around parsing primitives could not see it. It pins
|
||||
`docs/USER-GUIDE/getting-started/quickstart.md` to the old vocabulary. Replacing `status: current`
|
||||
with `status: active` turned it red.
|
||||
|
||||
Updated to `status: active`, the contract's value for "in force", with the reason in a comment
|
||||
beside it. Verified by evaluating both regexes against the real file: old `false`, new `true`.
|
||||
Control: a page carrying `superseded-by` still fails the new regex, so the assertion still asserts
|
||||
something rather than matching anything with front matter.
|
||||
|
||||
**The method point, which outlives this file.** CI 2592 ran the whole suite against the stamped
|
||||
tree, 46 turbo tasks, and returned exactly one failing spec: this one. That is a stronger consumer
|
||||
search than any grep the three of us ran, because it does not depend on guessing how a consumer
|
||||
reads the file.
|
||||
|
||||
An earlier draft of this paragraph put a test count here, "1617 tests, 1 failed". **That number was
|
||||
wrong and it is withdrawn.** Extracting per-test totals from these pipeline logs is not reliable:
|
||||
the same regex over the same log format returns 1003 for 2592, 1022 for 2593 and 3471 for 2594,
|
||||
which are runs of the same suite. Three irreconcilable answers from one method is proof the method
|
||||
does not measure what it claims. What the log does carry reliably is the FAIL list and the turbo
|
||||
task line, so the claim is stated in those terms instead. The point never needed the count: one
|
||||
named failing spec is what refuted three hand-searches. **Run the suite before concluding a
|
||||
documentation change has no code consumers.** Two independent seats and a reviewer searching by
|
||||
hand missed the one that existed.
|
||||
|
||||
## fred's second pass: F withdrawn, Q1 answered
|
||||
|
||||
Both changes below are fred's rulings, applied. Neither is my judgement and I record whose it is.
|
||||
|
||||
### F is withdrawn: `channel-protocol.md` returns to `kind: guide`
|
||||
|
||||
Reverted. fred re-measured the file himself with a control and reached the count I reported: zero
|
||||
uppercase RFC2119 terms case-sensitive, seven lowercase `must`, every one disclaiming authority,
|
||||
under a banner that refuses requirements status. A page like that is a guide.
|
||||
|
||||
The reason this matters past one stamp is fred's own: F rested on "the doc graph outranks the
|
||||
page's own banner", which contradicts his decision D, "kind follows content, never the filename and
|
||||
never what other documents say about it". D is the rule. F was a counterexample to it, written in
|
||||
the same comment.
|
||||
|
||||
Neither rescue was taken. vision's adjective edit on the three citing docs and an edit to the
|
||||
page's banner would both have preserved a stamp that should not have been made. The three citing
|
||||
documents stay as they are: under vision's index-pointer reading, "canonical" claims the best page
|
||||
on a subject rather than normative force, so there is nothing to correct.
|
||||
|
||||
**What this costs the contract: nothing, and that is the point.** A kind that survives only by
|
||||
editing the evidence around it is not a classification.
|
||||
|
||||
### Q1 answered: `docs/README.md` is stamped `kind: spec`, `source_of_truth: true`
|
||||
|
||||
Applied. By D, a document that prescribes the contract has spec content. By B, `source_of_truth` is
|
||||
an orthogonal boolean and this is the authoritative statement of the contract, so it carries both.
|
||||
|
||||
The authority half of Q1 was whether stamping the README settles a conflict with the flatten plan,
|
||||
whose enum still has seven values against the README's six. fred's ruling: **a plan never outranks
|
||||
the artifact it planned.** The plan's enum is pre-decision-B staleness, not a competing authority.
|
||||
The plan is fred's file and he corrects it there.
|
||||
|
||||
The contract now applies to the document that states it. That was the only state in which it could
|
||||
be credible.
|
||||
|
||||
### Verification arithmetic, re-closed
|
||||
|
||||
128 live `.md` under `docs/` = **108 stamped** + 16 operator-held + 3 supersede deferrals + 1
|
||||
generated. The stamped count rose by one and the operator-held set fell by one, both because of the
|
||||
README; every other row is unchanged. Control unchanged: the verifier reports `valid=False` when a
|
||||
kind is corrupted to `nonsense`.
|
||||
|
||||
### One consumer finding that is not a defect
|
||||
|
||||
A sweep of every spec reading a path under `docs/` returns 10 files. Four read a live file:
|
||||
`fleet-north-star.spec.ts` and `installation-documentation.spec.ts` (both already caught by CI),
|
||||
`mutator-gate.acceptance.spec.ts` reading `compaction-revocation.md`, which passes under its `guide`
|
||||
stamp, and `roster-v2.spec.ts:366`, which reads `docs/fleet/reference/roster-v2.schema.json`.
|
||||
|
||||
The fourth is a real read of a real live file under `docs/` and is untouched only because decision E
|
||||
scopes the contract to `.md`. Had the contract covered every file under `docs/`, front matter in
|
||||
that JSON schema would have broken the spec, the same failure as the quickstart regex. E was
|
||||
load-bearing beyond the YAML-parse grounds it was decided on. No action; recorded so the `.md`
|
||||
boundary is not widened later without re-running this sweep.
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Documentation Plans
|
||||
|
||||
> **Status:** Current artifact index. Plans record approved intent and execution approach; they are not current product behavior or operational authority.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: tracking
|
||||
status: active
|
||||
---
|
||||
|
||||
# Tasks — Release Integrity Workstream (RI-050, #1275)
|
||||
|
||||
> Single-writer: the RI-050 orchestrator (jarvis, dragon-lin) only. Workers read but never modify.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: guide
|
||||
status: active
|
||||
---
|
||||
|
||||
# Documentation Reports
|
||||
|
||||
> **Status:** Current evidence index. Reports record reviews, tests, audits, and deferred findings; they are not requirements or operational instructions by themselves.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: record
|
||||
status: active
|
||||
---
|
||||
|
||||
# Independent Code Review — #756 Official Discord Channel Plugin
|
||||
|
||||
**Verdict: APPROVE**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: record
|
||||
status: active
|
||||
---
|
||||
|
||||
# Code Review Report — Gateway Security Hardening
|
||||
|
||||
## Scope Reviewed
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: record
|
||||
status: active
|
||||
---
|
||||
|
||||
# #830 Documentation Completion Checklist
|
||||
|
||||
## Required artifacts
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: record
|
||||
status: active
|
||||
---
|
||||
|
||||
# FCM-M5-001 Fleet Documentation Deferrals and Holds
|
||||
|
||||
**Issue:** #758 · **Branch:** `docs/758-fleet-config-operator-docs`
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: record
|
||||
status: active
|
||||
---
|
||||
|
||||
# Mosaic Stack Documentation Catalog and Truth Audit
|
||||
|
||||
> **Status:** First-pass static audit — 2026-08-10
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: record
|
||||
status: active
|
||||
---
|
||||
|
||||
# Documentation Completion Checklist — #756 Official Discord plugin
|
||||
|
||||
## Required artifacts
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: record
|
||||
status: active
|
||||
---
|
||||
|
||||
# FCM-M5-001 Fleet Documentation IA Closure Evidence
|
||||
|
||||
**Issue:** #758 · **Task:** FCM-M5-001
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: record
|
||||
status: active
|
||||
---
|
||||
|
||||
# Mosaic Framework Consistency Audit
|
||||
|
||||
Date: 2026-02-17
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: record
|
||||
status: active
|
||||
---
|
||||
|
||||
VERDICT: GO
|
||||
|
||||
# Native Kanban/SOT canon independent re-review 2
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: record
|
||||
status: active
|
||||
---
|
||||
|
||||
# Independent Review — Native Kanban/SOT Canon
|
||||
|
||||
**Reviewer:** `enhance-sol` (independent of author `planner-sol`)
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: record
|
||||
status: active
|
||||
---
|
||||
|
||||
# KBN-101 contract independent security/architecture review
|
||||
|
||||
**Verdict: REQUEST CHANGES**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: record
|
||||
status: active
|
||||
---
|
||||
|
||||
# #751 Native Kanban/SOT canonical publication — Ultron final gate
|
||||
|
||||
**Verdict: GO** — zero BLOCKER/HIGH findings.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: record
|
||||
status: active
|
||||
---
|
||||
|
||||
# QA Report — Gateway Security Hardening
|
||||
|
||||
## Scope
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: record
|
||||
status: active
|
||||
---
|
||||
|
||||
# Performance Optimization — P8-003
|
||||
|
||||
**Branch:** `feat/p8-003-performance`
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: record
|
||||
status: active
|
||||
---
|
||||
|
||||
# #1099 pipefail + early-exit sweep
|
||||
|
||||
Baseline: `df4c591ab42aa1ae62c12935fdc0e772684864a0`
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
kind: record
|
||||
status: active
|
||||
---
|
||||
|
||||
# Security Review — Issue #756
|
||||
|
||||
**Scope:** final current uncommitted Discord plugin, shared channel contract, gateway ingress, AgentService, and plugin registration delta
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user