@shardworks/astrolabe-apparatus 0.1.273 → 0.1.275

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"plan-finalize.d.ts","sourceRoot":"","sources":["../../src/engines/plan-finalize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAqC,MAAM,kCAAkC,CAAC;AACxG,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,8BAA8B,CAAC;AAKzD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAqB3C,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,YAAY,CA6ExF"}
1
+ {"version":3,"file":"plan-finalize.d.ts","sourceRoot":"","sources":["../../src/engines/plan-finalize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAqC,MAAM,kCAAkC,CAAC;AACxG,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,8BAA8B,CAAC;AAKzD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAmB3C,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,YAAY,CA6ExF"}
@@ -38,14 +38,11 @@ const FRAMEWORK_EMITTER = 'framework';
38
38
  * installed (astrolabe declares clockworks in `recommends`, not
39
39
  * `requires`). Mirrors the lazy resolution `summon()` and the animator's
40
40
  * `tryResolveClockworks` use for `LoomApi` and `ClockworksApi`.
41
+ * Delegates to the framework's `tryApparatus<T>` primitive — the
42
+ * optional-dependency counterpart to `apparatus<T>`.
41
43
  */
42
44
  function tryResolveClockworks() {
43
- try {
44
- return guild().apparatus('clockworks');
45
- }
46
- catch {
47
- return null;
48
- }
45
+ return guild().tryApparatus('clockworks');
49
46
  }
50
47
  export function createPlanFinalizeEngine(getPlansBook) {
51
48
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"plan-finalize.js","sourceRoot":"","sources":["../../src/engines/plan-finalize.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"plan-finalize.js","sourceRoot":"","sources":["../../src/engines/plan-finalize.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shardworks/astrolabe-apparatus",
3
- "version": "0.1.273",
3
+ "version": "0.1.275",
4
4
  "license": "ISC",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,18 +20,18 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "zod": "4.3.6",
23
- "@shardworks/stacks-apparatus": "0.1.273",
24
- "@shardworks/tools-apparatus": "0.1.273",
25
- "@shardworks/spider-apparatus": "0.1.273",
26
- "@shardworks/fabricator-apparatus": "0.1.273",
27
- "@shardworks/clerk-apparatus": "0.1.273",
28
- "@shardworks/loom-apparatus": "0.1.273",
29
- "@shardworks/clockworks-apparatus": "0.1.273",
30
- "@shardworks/animator-apparatus": "0.1.273"
23
+ "@shardworks/tools-apparatus": "0.1.275",
24
+ "@shardworks/stacks-apparatus": "0.1.275",
25
+ "@shardworks/clerk-apparatus": "0.1.275",
26
+ "@shardworks/spider-apparatus": "0.1.275",
27
+ "@shardworks/fabricator-apparatus": "0.1.275",
28
+ "@shardworks/clockworks-apparatus": "0.1.275",
29
+ "@shardworks/animator-apparatus": "0.1.275",
30
+ "@shardworks/loom-apparatus": "0.1.275"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "25.5.0",
34
- "@shardworks/nexus-core": "0.1.273"
34
+ "@shardworks/nexus-core": "0.1.275"
35
35
  },
36
36
  "files": [
37
37
  "dist",
@@ -15,6 +15,37 @@
15
15
  var LIMIT = 20;
16
16
  var writTitleLookup = {};
17
17
 
18
+ // ── URL handling ───────────────────────────────────────────────────────
19
+
20
+ /**
21
+ * Read the current querystring as a `URLSearchParams`. Live snapshot
22
+ * — read at call time, never cached.
23
+ */
24
+ function currentUrlParams() {
25
+ return new URLSearchParams(window.location.search);
26
+ }
27
+
28
+ /**
29
+ * Apply the given key/value changes to the current querystring and
30
+ * `pushState` the result. Null/undefined/empty value deletes the key.
31
+ * Mirrors Ratchet's `updateUrl` (D9). The plan param shape (?plan=ID)
32
+ * is preserved verbatim — operators have URLs in the wild already
33
+ * (D10).
34
+ */
35
+ function updateUrl(changes) {
36
+ var params = currentUrlParams();
37
+ var keys = Object.keys(changes);
38
+ for (var i = 0; i < keys.length; i++) {
39
+ var key = keys[i];
40
+ var value = changes[key];
41
+ if (value === null || value === undefined || value === '') params.delete(key);
42
+ else params.set(key, value);
43
+ }
44
+ var qs = params.toString();
45
+ var next = window.location.pathname + (qs ? '?' + qs : '');
46
+ window.history.pushState({}, '', next);
47
+ }
48
+
18
49
  // ── Utility ────────────────────────────────────────────────────────────
19
50
 
20
51
  function esc(s) {
@@ -299,11 +330,41 @@
299
330
 
300
331
  // ── Detail View ────────────────────────────────────────────────────────
301
332
 
302
- function showPlanDetail(plan) {
333
+ /**
334
+ * Render a "not found" empty state inside the plan detail view for a
335
+ * deep-linked id that does not resolve. Per D16 the URL param is
336
+ * preserved so the operator can recover (correct the id, hit Back).
337
+ * Replaces the legacy fall-back-to-list behaviour, which silently
338
+ * dropped the param and pretended the deep-link never happened.
339
+ */
340
+ function renderPlanDetailNotFound(planId) {
341
+ currentPlan = null;
342
+ listView.style.display = 'none';
343
+ detailView.style.display = '';
344
+ detailTitle.textContent = 'Plan not found';
345
+ metaCard.innerHTML =
346
+ '<div class="empty-state" style="padding:1.5rem">' +
347
+ 'No plan with id <code>' + esc(planId) + '</code> exists. ' +
348
+ 'It may have been deleted, or the id may be mistyped.</div>';
349
+ // Hide tab content / decisions table — they reference fields on a
350
+ // missing plan that would just render blanks.
351
+ var tabContent = document.getElementById('tab-content');
352
+ if (tabContent) tabContent.innerHTML = '';
353
+ }
354
+
355
+ function showPlanDetail(plan, opts) {
356
+ var skipUrlPush = !!(opts && opts.skipUrlPush);
303
357
  currentPlan = plan;
304
358
  listView.style.display = 'none';
305
359
  detailView.style.display = '';
306
360
 
361
+ // Centralised URL push — every entry path into showPlanDetail
362
+ // (row click, deep-link init, popstate-driven re-open) emits
363
+ // ?plan=ID for free. The popstate-driven path passes
364
+ // skipUrlPush=true to avoid double-pushing the URL the browser
365
+ // already updated.
366
+ if (!skipUrlPush) updateUrl({ plan: plan.id });
367
+
307
368
  detailTitle.textContent = 'Plan: ' + plan.id;
308
369
 
309
370
  // Metadata card
@@ -605,39 +666,67 @@
605
666
 
606
667
  // ── Navigation ─────────────────────────────────────────────────────────
607
668
 
608
- function backToList() {
669
+ function backToList(opts) {
670
+ var skipUrlPush = !!(opts && opts.skipUrlPush);
609
671
  detailView.style.display = 'none';
610
672
  listView.style.display = '';
611
673
  currentPlan = null;
674
+ // D11: push a clean URL — the operator's Forward button still does
675
+ // what they expect, and we never pop history because they may have
676
+ // arrived directly at ?plan=ID with no prior list-view entry.
677
+ if (!skipUrlPush) updateUrl({ plan: null });
612
678
  }
613
679
 
614
680
  // ── Deep Link ──────────────────────────────────────────────────────────
615
681
 
616
- function handleDeepLink() {
617
- var params = new URLSearchParams(window.location.search);
618
- var planId = params.get('plan');
682
+ /**
683
+ * Resolve `?plan=ID` to a detail view. Called on init and from the
684
+ * popstate handler. Both paths suppress the URL push (the browser
685
+ * already has the URL in place, or the deep-link landed already).
686
+ * A missing/deleted/mistyped id renders the not-found empty state
687
+ * (D16) — the URL param is left intact.
688
+ *
689
+ * `opts.fetchOnEmpty` selects what to do when no plan param is
690
+ * present. Init wants the list to be fetched (this is the page's
691
+ * normal load); popstate just wants to switch back to whatever was
692
+ * already rendered.
693
+ */
694
+ function handleDeepLink(opts) {
695
+ var fetchOnEmpty = !(opts && opts.fetchOnEmpty === false);
696
+ var planId = currentUrlParams().get('plan');
619
697
 
620
698
  if (planId) {
621
- // Deep link: fetch and show the specific plan
622
699
  fetch('/api/plan/show?planId=' + encodeURIComponent(planId))
623
700
  .then(function (r) {
624
701
  if (!r.ok) throw new Error('HTTP ' + r.status);
625
702
  return r.json();
626
703
  })
627
704
  .then(function (plan) {
628
- showPlanDetail(plan);
705
+ showPlanDetail(plan, { skipUrlPush: true });
629
706
  })
630
707
  .catch(function (err) {
631
708
  console.error('Deep-link plan not found:', planId, err);
632
- // Fallback: show list view
633
- fetchPlans(true);
709
+ // D16: surface the failure as a not-found empty state inside
710
+ // the detail panel; never silently rewrite the URL.
711
+ renderPlanDetailNotFound(planId);
634
712
  });
635
- } else {
713
+ } else if (fetchOnEmpty) {
636
714
  fetchPlans(true);
715
+ } else {
716
+ // popstate to a no-?plan URL: just return to the list view
717
+ // without re-fetching.
718
+ backToList({ skipUrlPush: true });
637
719
  }
638
720
  }
639
721
 
640
722
  // ── Init ───────────────────────────────────────────────────────────────
641
723
 
724
+ // popstate listener — the browser updated the URL, so we re-run the
725
+ // deep-link routing without re-pushing. Pairs with the central push
726
+ // inside showPlanDetail (D11/D12).
727
+ window.addEventListener('popstate', function () {
728
+ handleDeepLink({ fetchOnEmpty: false });
729
+ });
730
+
642
731
  handleDeepLink();
643
732
  })();
@@ -708,3 +708,101 @@ describe('astrolabe.js cost-panel rig lookup uses rig-for-writ', () => {
708
708
  );
709
709
  });
710
710
  });
711
+
712
+ // ── Deep-link URL state (?plan=ID) ──────────────────────────────────────
713
+
714
+ describe('astrolabe.js — deep-link URL state', () => {
715
+ it('exposes currentUrlParams + updateUrl helpers (D9 — Ratchet pattern)', () => {
716
+ assert.match(
717
+ astrolabeJs,
718
+ /function currentUrlParams\(\)\s*\{[\s\S]*?new URLSearchParams\(window\.location\.search\)/,
719
+ 'currentUrlParams reads window.location.search live',
720
+ );
721
+ assert.match(
722
+ astrolabeJs,
723
+ /function updateUrl\(changes\)\s*\{[\s\S]*?window\.history\.pushState/,
724
+ 'updateUrl pushes via history.pushState',
725
+ );
726
+ });
727
+
728
+ it('showPlanDetail pushes ?plan=ID via the central updateUrl call (D12)', () => {
729
+ const block = astrolabeJs.match(
730
+ /function showPlanDetail\(plan(?:, opts)?\)[\s\S]*?(?=\n {2}function )/,
731
+ );
732
+ assert.ok(block, 'should find showPlanDetail body');
733
+ assert.match(
734
+ block[0],
735
+ /updateUrl\(\{\s*plan:\s*plan\.id\s*\}\)/,
736
+ 'showPlanDetail should push ?plan=<plan.id> when not skipUrlPush',
737
+ );
738
+ assert.match(block[0], /skipUrlPush/, 'showPlanDetail accepts a skipUrlPush opt');
739
+ });
740
+
741
+ it('backToList clears ?plan via updateUrl({plan: null}) — never pops history (D11)', () => {
742
+ const block = astrolabeJs.match(
743
+ /function backToList\((?:opts)?\)[\s\S]*?(?=\n {2}\/\/)/,
744
+ );
745
+ assert.ok(block, 'should find backToList body');
746
+ assert.match(
747
+ block[0],
748
+ /updateUrl\(\{\s*plan:\s*null\s*\}\)/,
749
+ 'backToList should push a clean URL via updateUrl({plan: null})',
750
+ );
751
+ assert.ok(
752
+ !/window\.history\.back\s*\(/.test(block[0]),
753
+ 'backToList should never invoke history.back',
754
+ );
755
+ });
756
+
757
+ it('a popstate listener re-runs handleDeepLink without re-pushing the URL', () => {
758
+ assert.match(
759
+ astrolabeJs,
760
+ /window\.addEventListener\(\s*['"]popstate['"]/,
761
+ 'astrolabe.js registers a popstate listener',
762
+ );
763
+ const block = astrolabeJs.match(
764
+ /addEventListener\(\s*['"]popstate['"][\s\S]*?\}\)/,
765
+ );
766
+ assert.ok(block, 'should find popstate handler body');
767
+ assert.match(
768
+ block[0],
769
+ /handleDeepLink\(\{\s*fetchOnEmpty:\s*false\s*\}\)/,
770
+ 'popstate handler invokes handleDeepLink with fetchOnEmpty=false',
771
+ );
772
+ });
773
+
774
+ it('handleDeepLink renders a not-found state instead of falling back to the list (D16)', () => {
775
+ const block = astrolabeJs.match(
776
+ /function handleDeepLink\([\s\S]*?(?=\n {2}\/\/ ── Init)/,
777
+ );
778
+ assert.ok(block, 'should find handleDeepLink body');
779
+ assert.match(
780
+ block[0],
781
+ /renderPlanDetailNotFound\(planId\)/,
782
+ 'handleDeepLink should render the not-found state on fetch failure',
783
+ );
784
+ // The deep-link calls into showPlanDetail with skipUrlPush so we
785
+ // don't double-push the URL the operator already arrived with.
786
+ assert.match(
787
+ block[0],
788
+ /showPlanDetail\(plan,\s*\{\s*skipUrlPush:\s*true\s*\}\)/,
789
+ 'init / popstate path passes skipUrlPush=true into showPlanDetail',
790
+ );
791
+ });
792
+
793
+ it('renderPlanDetailNotFound preserves the URL param', () => {
794
+ const block = astrolabeJs.match(
795
+ /function renderPlanDetailNotFound\([\s\S]*?(?=\n {2}function )/,
796
+ );
797
+ assert.ok(block, 'should find renderPlanDetailNotFound body');
798
+ assert.ok(
799
+ !/updateUrl/.test(block[0]),
800
+ 'renderPlanDetailNotFound must not rewrite the URL',
801
+ );
802
+ assert.match(
803
+ block[0],
804
+ /No plan with id/,
805
+ 'renderPlanDetailNotFound surfaces a "not found" message',
806
+ );
807
+ });
808
+ });
@@ -43,7 +43,7 @@ You also have the standard file-reading tools (Read, Glob, Grep) for exploring t
43
43
  3. Write the codebase inventory using `inventory-write`. The inventory must meet the full quality bar described below.
44
44
  4. Write scope items using `scope-write`. Break the brief into coarse, independently deliverable capabilities. Each item should be something the patron might include or exclude.
45
45
  5. Write decisions using `decisions-write`. Be exhaustive — capture every design question including ones where the answer seems obvious from codebase conventions. Each decision needs: id, scope references, question, context, options, recommendation, rationale, and `selected`. Pre-fill `selected` on every decision — use brief pre-emption and suggestion rules where they apply, and apply the Three Defaults everywhere else. Never set `patronOverride` — that field is owned by the patron-review pass. When you feel uncertainty about any decision, treat that feeling as a cue to **investigate** — read more code, trace another caller, check the brief again — not as a cue to leave `selected` unset.
46
- 6. Write observations using `observations-write`. Record refactoring opportunities, risks, suboptimal conventions, doc/code discrepancies, and potential bugs noticed during your pass.
46
+ 6. Write observations using `observations-write`. **Apply the discriminating bar in the *Observations* section below** — every record you lift will be auto-promoted to a draft writ, so the bar for "this deserves an observation" is high. Doc drift on touched files is NOT an observation (note it in the inventory as "concurrent doc updates needed" instead). Brief meta-observations and future-feature placeholders are NOT observations.
47
47
 
48
48
  You may interleave reading and writing — for example, write partial inventory as you go and refine it, or write scope items as they become clear and adjust later. The key constraint is that when you finish, all four artifacts (inventory, scope, decisions, observations) must be complete and written to the plan via the write tools.
49
49
 
@@ -92,7 +92,8 @@ Your inventory feeds a downstream spec writer who produces **intent-based briefs
92
92
  - Any prior commissions that touched this code (check commission log if relevant)
93
93
 
94
94
  **Doc/code discrepancies:**
95
- - Note any places where documentation describes different behavior than the code implements. These may indicate bugs, stale docs, or unfinished migrations. Don't try to resolve them just record them.
95
+ - Note any places where documentation describes different behavior than the code implements. Capture them in the inventory as data points; do NOT lift to observations unless they meet the *Observations* bar (real bug, real cross-cutting design Q, real consolidation, real hidden-migration evidence).
96
+ - **Tag drift on files the commission will already be touching as `concurrent doc updates needed`** — the implementing artificer will fix this inline as part of the work. Do not separately lift it as an observation.
96
97
 
97
98
  This is a working document — rough, thorough, and unpolished. Do not spend effort on formatting or prose quality. Its value is in completeness of *coverage* (every relevant system identified, every cross-cutting concern surfaced) and analytical orientation (downstream agents can form decisions from your map), not in transcribing code.
98
99
 
@@ -185,12 +186,24 @@ Order decisions by scope item.
185
186
 
186
187
  ### Observations
187
188
 
188
- Accumulate a punch list of things noticed during your pass that are outside the brief's scope but worth recording:
189
+ Observations are concerns worth lifting to a draft mandate writ for downstream curator review. Each observation gets auto-promoted to a draft writ, so the bar for what counts as an observation must be high — anything you lift will sit in the books until a curator triages it.
189
190
 
190
- - **Refactoring opportunities** skipped to keep scope narrow
191
- - **Suboptimal conventions** followed for consistency
192
- - **Doc/code discrepancies** found during inventory
193
- - **Potential bugs or risks** noticed in adjacent code
191
+ **An observation is the right primitive when one of these is true:**
192
+
193
+ - **Real bug or latent hazard.** A code path that's wrong, a race that's possible, an edge case that will silently misbehave, a contract gap downstream consumers will trip over.
194
+ - **Real cross-cutting design question.** A decision that needs to be made because two or more apparatuses will trip over it. Not a question for *this* commission (those are decisions, not observations) but one that surfaces during this pass and deserves its own thread.
195
+ - **Real DRY/consolidation opportunity with concrete payoff.** Duplicated logic across N call sites with measurable maintenance cost. Not "this could be cleaner" but "this WILL drift and bite us."
196
+ - **Doc/code discrepancy that points at a hidden bug or unfinished migration.** Where the gap implies the migration was abandoned mid-way or a behavior was changed without updating callers.
197
+
198
+ **An observation is NOT the right primitive for any of these:**
199
+
200
+ - **Doc drift on files inside or adjacent to your touched area.** Surface in inventory under "concurrent doc updates needed" so the implementing artificer fixes it inline. Stale text in `clockworks.md` while the commission is already editing `clockworks.md` is part of the work, not a follow-up.
201
+ - **Doc drift on files far outside your touched area.** Let the next commission that touches them fix the drift. Doc drift is largely self-healing through normal traffic.
202
+ - **Brief meta-observations.** "The brief cites stale line numbers" / "the brief mislocates X." These are observations about the staleness of the planning artifact, which becomes archival once the commission ships. Do not lift.
203
+ - **Future-feature placeholders.** "Someone should commission X downstream." Track those as clicks under the appropriate subtree, not as observation writs.
204
+ - **Nice-to-have UX or polish** without observed friction. Wait for a real operator/anima to hit the issue.
205
+
206
+ When in doubt about whether to lift, ask: *"Could the artificer of this commission realistically address this inline?"* If yes, surface it in the inventory rather than as an observation. *"Will this self-heal as someone touches the area next?"* If yes, do not lift.
194
207
 
195
208
  Each observation is **one record per atomic concern**. Downstream, the `astrolabe.observation-lift` engine lifts each record into a draft top-level `mandate` writ (never a child of the originating mandate); each lifted writ carries an `astrolabe.lifted-from` provenance edge back to the originating mandate, plus a `spider.follows` edge that holds dispatch until the mandate has terminated. When the plan yields two or more observations, the engine additionally groups them under a top-level `observation-set` container that parents the draft mandates and carries the `astrolabe.lifted-from` edge on behalf of the batch. A curator (human or automated) promotes each draft to open status. Your job is to package the concerns; you do not decide which ones get promoted.
196
209
 
@@ -60,7 +60,8 @@ Your inventory feeds a downstream scoping primer and spec writer who produce **i
60
60
  - Any prior commissions that touched this code (check commission log if relevant)
61
61
 
62
62
  **Doc/code discrepancies:**
63
- - Note any places where documentation describes different behavior than the code implements. These may indicate bugs, stale docs, or unfinished migrations. Don't try to resolve them just record them.
63
+ - Note any places where documentation describes different behavior than the code implements. Capture them in the inventory as data points; downstream primer stages will decide whether any rise to the bar of being a separately-lifted observation.
64
+ - **Tag drift on files the commission will already be touching as `concurrent doc updates needed`** — the implementing artificer will fix this inline as part of the work, no separate observation needed.
64
65
 
65
66
  **Click references in the brief:**
66
67
  - Briefs frequently reference clicks by id (long form `c-mo2e88aw-f4d5684cf385` or short form `c-mo301yp9`). Clicks are the guild's record of decisions and open inquiries, managed by the Ratchet apparatus. Treat click references as mandatory context — same priority as reading referenced source files.
@@ -181,12 +181,24 @@ Write all decisions using `decisions-write`.
181
181
 
182
182
  ### Step 3: Observations
183
183
 
184
- Accumulate a punch list of things noticed during analysis that are outside the brief's scope but worth recording:
184
+ Observations are concerns worth lifting to a draft mandate writ for downstream curator review. Each observation gets auto-promoted to a draft writ, so the bar for what counts as an observation must be high — anything you lift will sit in the books until a curator triages it.
185
185
 
186
- - **Refactoring opportunities** skipped to keep scope narrow
187
- - **Suboptimal conventions** followed for consistency
188
- - **Doc/code discrepancies** found during inventory
189
- - **Potential bugs or risks** noticed in adjacent code
186
+ **An observation is the right primitive when one of these is true:**
187
+
188
+ - **Real bug or latent hazard.** A code path that's wrong, a race that's possible, an edge case that will silently misbehave, a contract gap downstream consumers will trip over.
189
+ - **Real cross-cutting design question.** A decision that needs to be made because two or more apparatuses will trip over it. Not a question for *this* commission (those are decisions, not observations) but one that surfaces during this pass and deserves its own thread.
190
+ - **Real DRY/consolidation opportunity with concrete payoff.** Duplicated logic across N call sites with measurable maintenance cost. Not "this could be cleaner" but "this WILL drift and bite us."
191
+ - **Doc/code discrepancy that points at a hidden bug or unfinished migration.** Where the gap implies the migration was abandoned mid-way or a behavior was changed without updating callers.
192
+
193
+ **An observation is NOT the right primitive for any of these:**
194
+
195
+ - **Doc drift on files inside or adjacent to your touched area.** Surface in inventory under "concurrent doc updates needed" so the implementing artificer fixes it inline. Stale text in `clockworks.md` while the commission is already editing `clockworks.md` is part of the work, not a follow-up.
196
+ - **Doc drift on files far outside your touched area.** Let the next commission that touches them fix the drift. Doc drift is largely self-healing through normal traffic.
197
+ - **Brief meta-observations.** "The brief cites stale line numbers" / "the brief mislocates X." These are observations about the staleness of the planning artifact, which becomes archival once the commission ships. Do not lift.
198
+ - **Future-feature placeholders.** "Someone should commission X downstream." Track those as clicks under the appropriate subtree, not as observation writs.
199
+ - **Nice-to-have UX or polish** without observed friction. Wait for a real operator/anima to hit the issue.
200
+
201
+ When in doubt about whether to lift, ask: *"Could the artificer of this commission realistically address this inline?"* If yes, surface it in the inventory rather than as an observation. *"Will this self-heal as someone touches the area next?"* If yes, do not lift.
190
202
 
191
203
  Each observation is **one record per atomic concern**. Downstream, the `astrolabe.observation-lift` engine lifts each record into a draft top-level `mandate` writ (never a child of the originating mandate); each lifted writ carries an `astrolabe.lifted-from` provenance edge back to the originating mandate, plus a `spider.follows` edge that holds dispatch until the mandate has terminated. When the plan yields two or more observations, the engine additionally groups them under a top-level `observation-set` container that parents the draft mandates and carries the `astrolabe.lifted-from` edge on behalf of the batch. A curator (human or automated) promotes each draft to open status. Your job is to package the concerns; you do not decide which ones get promoted.
192
204
 
@@ -43,7 +43,7 @@ You also have the standard file-reading tools (Read, Glob, Grep) for exploring t
43
43
  3. Write the codebase inventory using `inventory-write`. The inventory must meet the full quality bar described below.
44
44
  4. Write scope items using `scope-write`. Break the brief into coarse, independently deliverable capabilities. Each item should be something the patron might include or exclude.
45
45
  5. Write decisions using `decisions-write`. Be exhaustive — capture every design question including ones where the answer seems obvious from codebase conventions. Each decision needs: id, scope references, question, context, options, recommendation, rationale, and `selected` (see the pre-fill rule under Decision Analysis — leave unset only when the decision matches the razor and passes the Reach and Patch Tests; otherwise apply the three defaults and pre-fill with your choice). Never set `patronOverride` — that field is owned by the patron-review pass. When you feel uncertainty about a decision that does *not* match any razor criterion, treat that feeling as a cue to **investigate** — read more code, trace another caller, check the brief again — not as a cue to punt the decision to the patron.
46
- 6. Write observations using `observations-write`. Record refactoring opportunities, risks, suboptimal conventions, doc/code discrepancies, and potential bugs noticed during your pass.
46
+ 6. Write observations using `observations-write`. **Apply the discriminating bar in the *Observations* section below** — every record you lift will be auto-promoted to a draft writ, so the bar for "this deserves an observation" is high. Doc drift on touched files is NOT an observation (note it in the inventory as "concurrent doc updates needed" instead). Brief meta-observations and future-feature placeholders are NOT observations.
47
47
 
48
48
  You may interleave reading and writing — for example, write partial inventory as you go and refine it, or write scope items as they become clear and adjust later. The key constraint is that when you finish, all four artifacts (inventory, scope, decisions, observations) must be complete and written to the plan via the write tools.
49
49
 
@@ -92,7 +92,8 @@ Your inventory feeds a downstream spec writer who produces **intent-based briefs
92
92
  - Any prior commissions that touched this code (check commission log if relevant)
93
93
 
94
94
  **Doc/code discrepancies:**
95
- - Note any places where documentation describes different behavior than the code implements. These may indicate bugs, stale docs, or unfinished migrations. Don't try to resolve them just record them.
95
+ - Note any places where documentation describes different behavior than the code implements. Capture them in the inventory as data points; do NOT lift to observations unless they meet the *Observations* bar (real bug, real cross-cutting design Q, real consolidation, real hidden-migration evidence).
96
+ - **Tag drift on files the commission will already be touching as `concurrent doc updates needed`** — the implementing artificer will fix this inline as part of the work. Do not separately lift it as an observation.
96
97
 
97
98
  This is a working document — rough, thorough, and unpolished. Do not spend effort on formatting or prose quality. Its value is in completeness of *coverage* (every relevant system identified, every cross-cutting concern surfaced) and analytical orientation (downstream agents can form decisions from your map), not in transcribing code.
98
99
 
@@ -216,12 +217,24 @@ Order decisions by scope item.
216
217
 
217
218
  ### Observations
218
219
 
219
- Accumulate a punch list of things noticed during your pass that are outside the brief's scope but worth recording:
220
+ Observations are concerns worth lifting to a draft mandate writ for downstream curator review. Each observation gets auto-promoted to a draft writ, so the bar for what counts as an observation must be high — anything you lift will sit in the books until a curator triages it.
220
221
 
221
- - **Refactoring opportunities** skipped to keep scope narrow
222
- - **Suboptimal conventions** followed for consistency
223
- - **Doc/code discrepancies** found during inventory
224
- - **Potential bugs or risks** noticed in adjacent code
222
+ **An observation is the right primitive when one of these is true:**
223
+
224
+ - **Real bug or latent hazard.** A code path that's wrong, a race that's possible, an edge case that will silently misbehave, a contract gap downstream consumers will trip over.
225
+ - **Real cross-cutting design question.** A decision that needs to be made because two or more apparatuses will trip over it. Not a question for *this* commission (those are decisions, not observations) but one that surfaces during this pass and deserves its own thread.
226
+ - **Real DRY/consolidation opportunity with concrete payoff.** Duplicated logic across N call sites with measurable maintenance cost. Not "this could be cleaner" but "this WILL drift and bite us."
227
+ - **Doc/code discrepancy that points at a hidden bug or unfinished migration.** Where the gap implies the migration was abandoned mid-way or a behavior was changed without updating callers.
228
+
229
+ **An observation is NOT the right primitive for any of these:**
230
+
231
+ - **Doc drift on files inside or adjacent to your touched area.** Surface in inventory under "concurrent doc updates needed" so the implementing artificer fixes it inline. Stale text in `clockworks.md` while the commission is already editing `clockworks.md` is part of the work, not a follow-up.
232
+ - **Doc drift on files far outside your touched area.** Let the next commission that touches them fix the drift. Doc drift is largely self-healing through normal traffic.
233
+ - **Brief meta-observations.** "The brief cites stale line numbers" / "the brief mislocates X." These are observations about the staleness of the planning artifact, which becomes archival once the commission ships. Do not lift.
234
+ - **Future-feature placeholders.** "Someone should commission X downstream." Track those as clicks under the appropriate subtree, not as observation writs.
235
+ - **Nice-to-have UX or polish** without observed friction. Wait for a real operator/anima to hit the issue.
236
+
237
+ When in doubt about whether to lift, ask: *"Could the artificer of this commission realistically address this inline?"* If yes, surface it in the inventory rather than as an observation. *"Will this self-heal as someone touches the area next?"* If yes, do not lift.
225
238
 
226
239
  Each observation is **one record per atomic concern**. Downstream, the `astrolabe.observation-lift` engine lifts each record into a draft top-level `mandate` writ (never a child of the originating mandate); each lifted writ carries an `astrolabe.lifted-from` provenance edge back to the originating mandate, plus a `spider.follows` edge that holds dispatch until the mandate has terminated. When the plan yields two or more observations, the engine additionally groups them under a top-level `observation-set` container that parents the draft mandates and carries the `astrolabe.lifted-from` edge on behalf of the batch. A curator (human or automated) promotes each draft to open status. Your job is to package the concerns; you do not decide which ones get promoted.
227
240